目录
前言
在上一篇博客中,已经掌握了如何创建和启动一个 Java 线程。但是,当成百上千个线程同时在系统中,如果不了解它们的生命周期,不掌握它们抢夺资源的规矩,那么将会造成巨大的灾难。
一、线程的生命周期
很多教程喜欢用操作系统的 5 种状态来解释 Java 线程,这其实容易造成混淆。在 Java 的世界里(具体来说是 java.lang.Thread.State 枚举中),官方明确定义了线程的 6 种状态。
这 6 种状态的流转构成了线程的一生:
-
NEW (新建状态):
- 你刚
new出来一个Thread对象,但还没有调用start()方法。此时它还只是内存里的一个普通 Java 对象,操作系统还没为它分配底层线程资源。
- 你刚
-
RUNNABLE (可运行状态):
- 调用了
start()方法后进入此状态。注意,Java 中的RUNNABLE包含了操作系统的"就绪(Ready)"和"运行中(Running)"两种状态。处于这个状态的线程,可能正在 CPU 上狂奔,也可能正在排队等待操作系统的调度分配时间片。
- 调用了
-
BLOCKED (阻塞状态):
- 这是因为"锁"而停滞的状态。 当线程试图进入一个被
synchronized关键字保护的代码块,但这个锁正被其他线程霸占着,它就会进入BLOCKED状态,在门外排队。
- 这是因为"锁"而停滞的状态。 当线程试图进入一个被
-
WAITING (无限期等待状态):
- 线程主动罢工,除非被别人唤醒,否则永远等下去 。通常是因为调用了
Object.wait()(不带超时时间)、Thread.join()或者LockSupport.park()。它在等另一个线程执行特定的操作(比如调用notify())。
- 线程主动罢工,除非被别人唤醒,否则永远等下去 。通常是因为调用了
-
TIMED_WAITING (限期等待状态):
- 和
WAITING类似,但它有个结束时间。时间一到,就算没人叫它,它也会自己醒来。比如调用了Thread.sleep(1000),它就会在这个状态待上 1000 毫秒。
- 和
-
TERMINATED (终止/死亡状态):
- 线程的
run()方法正常执行完毕,或者因为发生未捕获的异常而意外终止。死亡的线程绝对不能再次调用start()。
- 线程的

二、线程的安全问题
1.什么是线程的安全问题
当多个线程 同时访问同一个共享资源 (例如同一个对象的成员变量、同一个数据库记录),并且至少有一个线程在对该资源进行修改操作时,如果程序的最终执行结果偏离了我们的预期,这就是线程安全问题。
引发问题的核心原因有三个:
-
可见性问题:一个线程修改了共享变量,另一个线程没能立刻看到最新值(CPU 缓存导致)。
-
原子性问题 :一个看似简单的操作(如
count++),在底层其实分成了"读取-计算-赋值"三步,如果在这三步中间被其他线程插队,数据就乱了。 -
有序性问题:编译器或 CPU 为了优化性能,擅自打乱了代码的执行顺序(指令重排)。
2.问题举例
需求:某电影院再卖票,共有100张票,而它有3个窗口卖票,设计一个程序模拟该电影院卖票。
首先定义一个卖票线程
java
public class MyThread extends Thread{
static int ticket = 0;
@Override
public void run() {
while(true){
if(ticket < 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(getName() + "正在卖第" + ticket + "张票!");
ticket ++;
}else {
break;
}
}
}
}

可以看到尽管我们将ticket设置为静态变量,程序还是会卖同一张票,这是因为一个线程修改了共享变量,另一个线程没能立刻看到最新值。
三、解决线程的安全问题
1.同步代码块
就像给门票上了一把锁,规定同一时刻只能有一个线程进去执行卖票操作,其他线程必须在门外等待。
格式:
synchronized(锁){
操作共享数据的代码
}
特点1:锁默认打开,有一个线程进去了,锁自动关闭
特点2:里面的代码全部执行完毕,线程出来,锁自动打开
java
public class MyThread extends Thread{
static int ticket = 1;
//锁对象一定要是唯一的
static Object obj = new Object();
@Override
public void run() {
while(true){
synchronized (obj){
if(ticket <= 100){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(getName() + "正在卖第" + ticket + "张票!");
ticket ++;
}else {
break;
}
}
}
}
}
细节1:synchronized锁必须在while(true)内部,因为如果在外面的话,如果窗口1抢夺到cpu资源,此时上锁,其他进程无法抢夺cpu,窗口1会在这个代码块内部一直执行,一直卖票,直到break,才会解锁,其他窗口才能进行抢夺cpu资源。
细节2:锁必须是唯一的,标志着锁住了一个共享的操作对象(数据库等)。
2.同步方法
就是把synchronized关键字加到方法上。
格式:
修饰符 synchronized 返回值类型 方法名(参数){......}
特点1:同步方法是锁住方法里面所有的代码
特点2:锁对象不能自己指定
非静态:this
静态:当前类的字节码文件对象
java
public class MyRunnable implements Runnable{
int ticket = 0;
@Override
public void run() {
while(true){
synchronized (MyRunnable.class){
if (method()) break;
}
}
}
private synchronized boolean method() {
if(ticket == 100){
return true;
}else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "正在卖第" + ticket +"张票");
ticket++;
}
return false;
}
}
3.lock锁
虽然我们理解了同步代码块和同步方法中的锁对象,但是我们无法体会到上锁和解锁操作,因为这两个操作是系统帮我们实现的,为了更清晰的表达锁的概念,JDK5后提供了一个新的锁对象,可以通过**lock()和unlock()**来获得锁和释放锁。
Lock只是一个接口,不能直接实例化,采用它的实现类ReenTrantLock来实例化
java
public class MyThread extends Thread {
static int ticket = 1;
//锁对象一定要是唯一的
static Object obj = new Object();
static Lock lock = new ReentrantLock();
@Override
public void run() {
while (true) {
// synchronized (obj){
lock.lock();
try {
if (ticket <= 100) {
Thread.sleep(100);
System.out.println(getName() + "正在卖第" + ticket + "张票!");
ticket++;
} else {
break;
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
lock.unlock();
}
// }
}
}
}