前言
Java语言定义了 6 种线程状态,在任意一个时间点中,一个线程只能只且只有其中的一种状态,并且可以通过特定的方法在不同状态之间进行转换。
今天,我们就详细聊聊这几种状态,以及在什么情况下会发生转换。
一、线程状态
要想知道Java线程都有哪些状态,我们可以直接来看 Thread
,它有一个枚举类 State
。
public class Thread {
public enum State {
/**
* 新建状态
* 创建后尚未启动的线程
*/
NEW,
/**
* 运行状态
* 包括正在执行,也可能正在等待操作系统为它分配执行时间
*/
RUNNABLE,
/**
* 阻塞状态
* 一个线程因为等待临界区的锁被阻塞产生的状态
*/
BLOCKED,
/**
* 无限期等待状态
* 线程不会被分配处理器执行时间,需要等待其他线程显式唤醒
*/
WAITING,
/**
* 限期等待状态
* 线程不会被分配处理器执行时间,但也无需等待被其他线程显式唤醒
* 在一定时间之后,它们会由操作系统自动唤醒
*/
TIMED_WAITING,
/**
* 结束状态
* 线程退出或已经执行完成
*/
TERMINATED;
}
}
二、状态转换
我们说,线程状态并非是一成不变的,可以通过特定的方法在不同状态之间进行转换。那么接下来,我们通过代码,具体来看看这些个状态是怎么形成的。
1、新建
新建状态最为简单,创建一个线程后,尚未启动的时候就处于此种状态。
public static void main(String[] args) {
Thread thread = new Thread("新建线程");
System.out.println("线程状态:"+thread.getState());
}
-- 输出:线程状态:NEW
2、运行
可运行线程的状态,当我们调用了start()
方法,线程正在Java虚拟机中执行,但它可能正在等待来自操作系统(如处理器)的其他资源。
所以,这里实际上包含了两种状态:Running 和 Ready
,统称为 Runnable
。这是为什么呢?
这里涉及到一个Java线程调度的问题:
线程调度,是指系统为线程分配处理器使用权的过程。调度主要方式有两种,协同式线程调度和抢占式线程调度。
- 协同式线程调度
线程的执行时间由线程本身来控制,线程把自己的工作执行完毕之后,要主动通知系统切换到另外一个线程上去。
- 抢占式线程调度
每个线程将由系统来自动分配执行时间,线程的切换不由线程本身来决定,是基于CPU时间分片的方式。
它们孰优孰劣,不在本文讨论范围之内。我们只需要知道,Java使用的线程调度方式就是抢占式调度。
通常,这个时间分片是很小的,可能只有几毫秒或几十毫秒。所以,线程的实际状态可能会在Running 和 Ready
状态之间不断变化。所以,再去区分它们意义不大。
那么,我们再多想一下,如果Java线程调度方式是协同式调度,也许再去区分这两个状态就很有必要了。
public static void main(String[] args) {
Thread thread = new Thread(() -> {
for (;;){}
});
thread.start();
System.out.println("线程状态:"+thread.getState());
}
-- 输出:线程状态:RUNNABLE
简单来看,上面的代码就使线程处于Runnable
状态。但值得我们注意的是,如果一个线程在等待阻塞I/O的操作时,它的状态也是Runnable
的。
我们来看两个经典阻塞IO的例子:
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
try {
ServerSocket serverSocket = new ServerSocket(9999);
while (true){
Socket socket = serverSocket.accept();
OutputStream outputStream = socket.getOutputStream();
outputStream.write("Hello".getBytes());
outputStream.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
},"accept");
t1.start();
Thread t2 = new Thread(() -> {
try {
Socket socket = new Socket("127.0.0.1",9999);
for (;;){
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[5];
inputStream.read(bytes);
System.out.println(new String(bytes));
}
} catch (IOException e) {
e.printStackTrace();
}
},"read");
t2.start();
}
上面的代码中,我们知道,serverSocket.accept()
和inputStream.read(bytes);
都是阻塞式方法。
它们一个在等待客户端的连接;一个在等待数据的到来。但是,这两个线程的状态却是 RUNNABLE
的。
"read" #13 prio=5 os_prio=0 tid=0x0000000023f6c800 nid=0x1cd0 runnable [0x0000000024b3e000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
"accept" #12 prio=5 os_prio=0 tid=0x0000000023f68000 nid=0x4cec runnable [0x0000000024a3e000]
java.lang.Thread.State: RUNNABLE
at java.net.DualStackPlainSocketImpl.accept0(Native Method)
at java.net.DualStackPlainSocketImpl.socketAccept(DualStackPlainSocketImpl.java:131)
at java.net.AbstractPlainSocketImpl.accept(AbstractPlainSocketImpl.java:409)
at java.net.PlainSocketImpl.accept(PlainSocketImpl.java:199)
这又是为什么呢 ?
我们前面说过,处于 Runnable 状态下的线程,正在 Java 虚拟机中执行,但它可能正在等待来自操作系统(如处理器)的其他资源
。
不管是CPU、网卡还是硬盘,这些都是操作系统的资源而已。当进行阻塞式的IO操作时,或许底层的操作系统线程确实处在阻塞状态,但在这里我们的 Java 虚拟机线程的状态还是 Runnable
。
不要小看这个问题,很具有迷惑性。有些面试官如果问到,如果一个线程正在进行阻塞式 I/O 操作时,它处于什么状态?是Blocked还是Waiting?
那这时候,我们就要义正言辞的告诉他:亲,都不是哦~
3、无限期等待
处于无限期等待状态下的线程,不会被分配处理器执行时间,除非其他线程显式的唤醒它。
最简单的场景就是调用了 Object.wait()
方法。
public static void main(String[] args) throws Exception {
Object object = new Object();
new Thread(() -> {
synchronized (object){
try {
object.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}}).start();
}
-- 输出:线程状态:WAITING
此时这个线程就处于无限期等待状态,除非有别的线程显式的调用object.notifyAll();
来唤醒它。
然后,就是Thread.join()
方法,当主线程调用了此方法,就必须等待子线程结束之后才能继续进行。
public static void main(String[] args) throws Exception {
Thread mainThread = new Thread(() -> {
Thread subThread = new Thread(() -> {
for (;;){}
});
subThread.start();
try {
subThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
mainThread.start();
System.out.println("线程状态:"+thread.getState());
}
//输出:线程状态:WAITING
如上代码,在主线程 mainThread
中调用了子线程的join()
方法,那么主线程就要等待子线程结束运行。所以此时主线程mainThread
的状态就是无限期等待。
多说一句,其实join()
方法内部,调用的也是Object.wait()
。
最后,我们说说LockSupport.park()
方法,它同样会使线程进入无限期等待状态。也许有的朋友对它很陌生,没有用过,我们来看一个阻塞队列的例子。
public static void main(String[] args) throws Exception {
ArrayBlockingQueue<Long> queue = new ArrayBlockingQueue(1);
Thread thread = new Thread(() -> {
while (true){
try {
queue.put(System.currentTimeMillis());
} catch (Exception e) {
e.printStackTrace();
}
}
});
thread.start();
}
如上代码,往往我们会通过阻塞队列的方式来做生产者-消费者模型的代码。
这里,ArrayBlockingQueue
长度为1,当我们第二次往里面添加数据的时候,发现队列已满,线程就会等待这里,它的源码里面正是调用了LockSupport.park()
。
同样的,这里也比较具有迷惑性,我来问你:阻塞队列中,如果队列为空或者队列已满,这时候执行take或者put操作的时候,线程的状态是 Blocked 吗?
那这时候,我们需要谨记这里的线程状态还是 WAITING
。它们之间的区别和联系,我们后文再看。
4、限期等待
同样的,处于限期等待状态下的线程,也不会被分配处理器执行时间,但是它在一定时间之后可以自动的被操作系统唤醒。
这个跟无限期等待的区别,仅仅就是有没有带有超时时间参数。
比如:
object.wait(3000);
thread.join(3000);
LockSupport.parkNanos(5000000L);
Thread.sleep(1000);
像这种操作,都会使线程处于限期等待的状态 TIMED_WAITING
。因为Thread.sleep()
必须带有时间参数,所以它不在无限期等待行列中。
5、阻塞
一个线程因为等待临界区的锁被阻塞产生的状态,也就是说,阻塞状态的产生是因为它正在等待着获取一个排它锁。
这里,我们来看一个 synchronized
的例子。
public static void main(String[] args) throws Exception {
Object object = new Object();
Thread t1 = new Thread(() -> {
synchronized (object){
for (;;){}
}
});
t1.start();
Thread t2 = new Thread(() -> {
synchronized (object){
System.out.println("获取到object锁,线程执行。");
}
});
t2.start();
System.out.println("线程状态:"+t2.getState());
}
//输出:线程状态:BLOCKED
我们看上面的代码,object对象锁一直被线程 t1 持有,所以线程 t2 的状态一直会是阻塞状态。
我们接着再来看一个锁的例子:
public static void main(String[] args){
Lock lock = new ReentrantLock();
lock.lock();
Thread t1 = new Thread(() -> {
lock.lock();
System.out.println("已获取lock锁,线程执行");
lock.unlock();
});
t1.start();
System.out.println("线程状态:"+t1.getState());
}
如上代码,我们有一个ReentrantLock
,main线程已经持有了这个锁,t1 线程会一直等待在lock.lock();
。
那么,此时 t1 线程的状态是什么呢 ?
其实答案是WAITING
,即无限期等待状态。这又是为什么呢 ?
原因在于,Lock
接口是Java API实现的锁,它的底层实现其实是抽象同步队列,简称AQS
。
在通过lock.lock()
获取锁的时候,如果锁正在被其他线程持有,那么线程会被放入AQS队列后,阻塞挂起。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
如果tryAcquire返回false,会把当前线程放入AQS阻塞队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
acquireQueued
方法会将当前线程放入 AQS 阻塞队列,然后调用LockSupport.park(this);
挂起线程。
所以,这也就解释了为什么lock.lock()
获取锁的时候,当前的线程状态会是 WAITING
。
常常有人会问,synchronized和Lock
的区别,除了一般性的答案,此时你也可以说一下线程状态的差异,我猜可能很少有人会意识到这一点。
6、结束
一个线程,当它退出或已经执行完成的时候,就是结束状态。
public static void main(String[] args) throws Exception {
Thread thread = new Thread(() -> System.out.println("线程已执行"));
thread.start();
Thread.sleep(1000);
System.out.println("线程状态:"+thread.getState());
}
//输出: 线程已执行
线程状态:TERMINATED
三、总结
本文介绍了 Java 线程的不同状态,以及在何种情况下发生转换。
原创不易,客官们点个赞再走嘛,这将是笔者持续写作的动力~