在Java领域,实现并发程序的主要手段就是多线程。线程是操作系统里的一个概念,Java语言里的线程本质上就是操作系统的线程,它们是一一对应的。
在操作系统层面,线程的生老病死叫做有生命周期。对于有生命周期的事物,重点在于搞懂生命周期中各个节点的状态转换机制。
通用的线程生命周期
通用的线程生命周期基本上可以用“五态模型”来描述:初始状态、可运行状态、运行状态、休眠状态、终止状态。
- 初始状态,指的是线程已经被创建,但是还不允许分配CPU执行。这个状态属于编程语言特有的,这里所谓的被创建,仅仅是在编程语言层面被创建,而在操作系统层面,真正的线程还没有创建。
- 可运行状态,指的是线程可以分配CPU执行。这种状态下,真正的操作系统线程已经被成功创建了,所以可以分配CPU执行。
- 当有空闲的CPU时,操作系统会将其分配给一个处于可运行状态的线程,被分配到CPU的线程的状态就转换成了运行状态。
- 运行状态的线程如果调用了一个阻塞的API(以阻塞方式读文件)或者等待某个事件(条件变量),那么线程的状态就会转换到休眠状态,同时释放CPU使用权,休眠状态的线程永远没有机会获得CPU使用权。当等待的事件出现了,线程就会从休眠状态转换到可运行状态。
- 线程执行完或者出现异常就会进入终止状态,终止状态的线程不会切换到其他任何状态,进入终止状态也就意味着线程的生命周期结束了。
这五种状态在不同编程语言里会有简化合并。C语言的POSIX Threads规范,把初始状态和可运行状态合并了;Java语言里则把可运行状态和运行状态合并了,这两个状态在操作系统调度层面有用,而JVM层面不关心这两个状态,因为JVM把线程调度交给操作系统处理了。
除了简化合并,这五种状态也有可能被细化。Java语言细化了休眠状态。
Java中线程的生命周期
Thread类中的一个变量:private volatile int threadStatus = 0;
Java语言中线程共有六种状态:
1 | public enum State { |
在操作系统层面,Java线程中的Blocked、Waiting、Timed_Waiting是一种状态,即休眠状态。只要Java线程处于这三种状态之一,那么这个线程就永远没有CPU的使用权。

从New到Runnable状态
Java刚创建出来的Thread对象就是New状态,而创建Thread对象主要有两种方法。
- 一种是继承Thread对象,重写run()方法。
1
2
3
4
5
6
7
8
9//自定义线程对象
class MyThread extends Thread {
public void run() {
//线程需要执行的代码
···
}
}
//创建线程对象
MyThread myThread = new MyThread(); - 另一种是实现Runnable接口,重写run()方法,并将该实现类作为创建Thread对象的参数。
1
2
3
4
5
6
7
8
9
10//实现Runnable接口
class Runner implements Runnable {
public void run() {
//线程需要执行的代码
···
}
}
//创建线程对象
Thread thread = new Thread(new Runner());
New状态的线程,不会被操作系统调度,因此不会执行。Java线程要执行,就必须转换到Runnable状态。从New状态转换到Runnable状态很简单,只要调用线程对象的start()方法就可以了。
1 | MyThread myThread = new MyThread(); |
Runnable与Blocked的状态转换
只有一种场景会触发这种转换,就是线程等待synchronized的隐式锁。synchronized修饰的方法/代码块同一时刻只允许一个线程执行,其他线程只能等待,这种情况下,未获取锁而等待的线程就会从Runnable转换到Blocked状态,并且进入锁对象的同步队列。而当等待的线程获得synchronized隐式锁时,就又会从Blocked转换到Runnable状态,重新争抢锁。
Java并发包中的锁Lock,都是基于LockSupport对象实现的。线程转换到等待状态。
阻塞态的线程不响应中断,并发包里的锁有方法能够响应中断。
线程调用阻塞式API时(bio,阻塞式的读文件,读网络数据,写socket等),在操作系统层面,线程是会转换到休眠状态的;但是在JVM层面,Java线程依然保持Runnable状态不会发生变化。JVM层面并不关心操作系统调度相关的状态,在JVM看来,等待CPU使用权(操作系统层面此时处于可执行状态)与等待I/O(操作系统层面此时处于休眠状态)没有区别,都是在等待某个资源,所以都归入了Runnable状态。
平时所谓的Java在调用阻塞式API(阻塞式的读文件,读网络数据)时,线程会阻塞,指的是操作系统线程的状态,并不是Java线程的状态。
Runnable与Waiting的状态转换
有三种场景会触发这种转换:
- 第一种场景,获得synchronized隐式锁的线程,调用无参数的Object.wait() 方法[管程:并发编程的万能钥匙]。先释放锁对象、再将线程状态变成Waiting、最后线程进入锁对象的等待队列。(当另一个线程,调用同一个对象的notify/notifyAll方法时,该线程被唤醒,从等待队列中移出,并从Waiting状态返回Runnable状态,重新争抢锁)
- 第二种场景,调用无参数的Thread.join()方法。其中的join()是一种线程同步方法,如有一个线程对象threadA,当调用A.join()的时候,执行这条语句的线程会等待threadA执行完,而等待中的这个线程,其状态会从Runnable转换到Waiting。当线程threadA执行完,原来等待它的线程又会从Waiting状态转换到Runnable。(Tread.join()源码的本质仍然是执行了wait()方法,锁对象是Thread A对象本身;Thread A结束后,由JVM自动调用A.notifyAll()来唤醒主线程继续执行)
- 第三种场景,调用LockSupport.park()方法。Java并发包中的锁,都是基于LockSupport对象实现的。调用LockSupport.park()【0/1】方法,当前线程会阻塞,线程的状态会从Runnable转换到Waiting。调用LockSupport.unpark(Thread thread)【1】可唤醒目标线程,目标线程的状态又会从Waiting状态转换到Runnable。
并发包中的Lock,线程会转换到等待状态。
Synchronized,线程转换到的是阻塞状态。
Runnable与Timed_waiting的状态转换
有五种场景会触发这种转换:
- 调用带超时参数的Thread.sleep(long millis)方法(仅仅让线程挂起,不释放锁);
- 获得synchronized隐式锁的线程,调用带超时参数的Object.wait(long timeout)方法;
- 调用带超时参数的Thread.join(long millis)方法;
- 调用带超时参数的LockSupport.parkNanos(Object blocker, long deadline)方法;
- 调用带超时参数的LockSupport.parkUntil(long deadling)方法。
Timed_waiting和Waiting状态的区别,仅仅是触发条件多了超时参数。
从Runnable到Terminated状态
线程执行完run()方法后,会自动转换到Terminated状态,当然如果执行run()方法的时候异常抛出,也会导致线程终止。有时候需要强制中断run()方法的执行,如run()方法访问一个很慢的网络,不想等下去了,Java的Thread类的stop()方法(已经标记为@Deprecated),正确的姿势是调用interrupt()方法。
stop()方法会真的杀死线程,不给线程喘息的机会,如果线程持有ReentrantLock锁,被stop()的线程并不会自动调用ReentrantLock的unlock()去释放锁,那其他线程就再也没有机会获得ReentrantLock锁,太危险所以该方法不建议使用了,类似的方法还有suspend()和resume()也不建议使用了。
Thread.stop()方法在结束线程时,会直接终止线程,并且会释放这个线程所持有的锁。
线程不安全,对共享对象进行了不完整的修改,直接stop,没有回滚的机会。
interrupt()方法仅仅是通知线程,线程有机会执行一些后续操作,同时也可以无视这个通知。被interrupt的线程,通过异常或者主动检测,收到通知。
interrupt不会结束线程的运行,在抛出InterruptedException后会清除中断标志。
当线程A处于Waiting、Timed_waiting状态时,
如果其他线程调用线程A的interrupt()方法,会使线程A返回到Runnable状态(中断A的Waiting和Timed_waiting状态),同时线程A的代码会触发InterruptedException异常。
转换到Waiting、Timed_waiting状态的触发条件,都是调用了类似wait()、join()、sleep()的方法,这些方法的签名都有throws InterruptedException这个异常。sleep、join等会立即抛出异常,wait在被唤醒之后才会抛出异常。
这个异常的触发条件是:其他线程调用了该线程的interrupt()方法。
当线程A处于Runnable状态,
并且阻塞在java.nio.channels.InterruptibleChannel上时,如果其他线程调用线程A的interrupt()方法,线程A会触发java.nio.channels.ClosedByInterruptException这个异常;
而阻塞在java.nio.channels.Selector上时,如果其他线程调用线程A的interrupt()方法,线程A的java.nio.channels.Selector会立即返回。
上面这两种情况属于被中断的线程通过异常的方式获得了通知。还有一种是主动监测,如果线程处于Runnable状态,并且没有阻塞在某个I/O操作上,如中断计算圆周率的线程A,这是就得依赖线程A主动检测中断状态了。如果其他线程调用线程A的interrupt()方法,那么线程A可以通过isInterrupted方法,检测是不是自己被中断了。
总结
理解Java线程的各种状态以及生命周期对于诊断多线程Bug非常有帮助,多线程Bug主要靠日志、线程dump来跟踪问题,分析线程状态是分析线程dump的基本功,大部分的死锁、活锁、饥饿问题都需要跟踪分析线程的状态。
可以通过jstack命令或者Java VisualVM这个可视化工具将JVM所有的线程栈信息导出来,完整的线程栈信息不仅包括线程的当前状态、调用栈,还包括了锁信息。导出线程栈,分析线程状态是诊断并发问题的一个重要工具。