0%

Java - 线程状态

在Java的线程生命周期中,一共有六种状态,定义在Thread.State枚举类型中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public enum State {
// A thread that has not yet started is in this state.
NEW,
// A thread executing in the Java virtual machine is in this state.
RUNNABLE,
// A thread that is blocked waiting for a monitor lock is in this state.
BLOCKED,
// A thread that is waiting indefinitely for another thread to perform a particular action is in this state.
WAITING,
// A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.
TIMED_WAITING,
// A thread that has exited is in this state.
TERMINATED;
}

NEW

线程对象创建之后,启动之前,这个时候的状态就是NEW。

RUNNABLE

调用了线程的start方法后,线程便进入RUNNABLE状态。

在JVM层面,只有一个RUNNABLE状态,表示线程正在Java虚拟机中执行任务,而放到操作系统层面的话,此时线程可能是正在执行,也有可能是准备就绪,等待系统资源,比如等待CPU调度,或者硬盘,网卡的资源。

现代操作系统架构,多采用时间分片的方式,进行抢占式轮转调度,有的还会加入优先级的机制,没有被调度到的线程,会被放到调度队列中,等待CPU调度,当获得时间片之后,便可以执行任务。

因此RUNNABLE状态,实际上是包含了线程在操作系统中,处于Rready和running的两个状态,只是JVM将调度的工作扔给了操作系统。

I/O阻塞

操作系统层面的相关知识,这里只简单描述一下,不过多深入。

一个I/O操作,对于CPU来说,是非常慢的,因此当CPU在运行过程中遇到一个I/O操作时,会从ready队列中调度另外一个线程来运行,然后将执行I/O的线程阻塞,放入等待队列中,注意这里是放入等待队列中,而不是放入ready队列中。

当I/O操作处理完毕时,会利用中断(interrupt)的机制来通知CPU,CPU不断检查中断,发现收到一个来自硬盘的中断信号之后,便会将之前放入等待队列中的线程放入ready队列中,等待再次被调度。

上面的逻辑,是操作系统层面的逻辑,那假设Java线程发起一个磁盘I/O操作,那么在上述逻辑过程中,Java线程会处于什么状态呢?或者说是否会在某些状态之间转换呢?我们来举个例子:

1
2
3
4
5
6
Scanner in = new Scanner(System.in);
Thread t1 = new Thread(() -> {
String input = in.nextLine();
System.out.println(input);
}, "T-1");
t1.start();

当代码执行到第三行的时候,我们打印线程状态,得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"T-1" #10 prio=5 os_prio=31 tid=0x00007fc45e1bd800 nid=0x5503 runnable [0x0000700008c2b000]
java.lang.Thread.State: RUNNABLE
at java.io.FileInputStream.readBytes(Native Method)
at java.io.FileInputStream.read(FileInputStream.java:255)
at java.io.BufferedInputStream.read1(BufferedInputStream.java:284)
at java.io.BufferedInputStream.read(BufferedInputStream.java:345)
- locked <0x000000076ab1d4d8> (a java.io.BufferedInputStream)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x000000076abd6f00> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.Reader.read(Reader.java:100)
at java.util.Scanner.readInput(Scanner.java:804)
at java.util.Scanner.findWithinHorizon(Scanner.java:1685)
at java.util.Scanner.nextLine(Scanner.java:1538)
at net.cllc.Main.lambda$main$0(Main.java:11)
at net.cllc.Main$$Lambda$1/2065951873.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)

线程处于RUNNABLE状态,同理也可以测试网络I/O,也是属于RUNNABLE状态。

此时线程对于CPU来说,属于阻塞状态,并没有被执行,或者等待调度执行,但是此时还有其他设备在工作,比如硬盘,网卡等等,这些设备有可能还在为线程服务。对于JVM来说,CPU,硬盘,网卡等设备,都是系统资源,只要有系统资源在为线程工作,那么线程在JVM中的状态,都是RUNNABLE,JVM并不关心线程在操作系统中的调度状态。

BLOCKED

BLOCKED状态,是指当前线程由于等待一个其他线程持有的监视器锁,导致当前线程处于阻塞状态。

当线程获取到锁之后,线程状态便会从BLOCKED切换成RUNNABLE。

WAITING

当一个线程调用了如下几个方法之一时,线程会进入无限期的WAITING状态:

  • 不带超时的Object.wait方法(Object.wait with no timeout);

  • 不带超时的Thread.join方法(Thread.join with no timeout);

  • LockSupport.part方法。

    注意,当线程进入到synchronized代码块的时候,如果需要等锁的话,会进入BLOCKED状态;但是如果使用java.util.concurrent.locks下的lock进行加锁的话,线程会进入WAITING/TIMED_WAITING状态,因为lock会调用LockSupport.part方法。

当有其他线程执行了如下几个方法之一时,进入WAITING状态的线程便会结束WAITING状态:

这里涉及的Object,需要跟进入WAITING状态时,调用wait方法的Object为同一个对象。

  • Object.notify方法;
  • Object.notifyAll方法;
  • 如果是调用的Thread.join方法进入的WAITING,则会等待join线程执行完毕。

从上面的描述中,不难理解,进入WAITING和解除WAITING,是两个线程的相互作用,当线程A由于某些条件不满足时候,会进入WAITING状态,等到其他线程将条件满足的时候,会调用notify方法,唤醒之前在等待的线程。

进入WAITING状态的线程,会释放当前已经持有的锁,同时不会占用CPU资源。

Lost Wake-Up Problem

调用Object.wait和Object.notify方法的代码,必须要在同步块中,并且同步块锁住的对象也要是调用方法的对象,否则会抛出IllegalMonitorStateException异常。如下两种代码都会抛出异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// #1
Object obj = new Object();
try{
obj.wait();
}catch(InterruptedException e){
// not in `synchronized`
e.printStackTrace();
}

// #2
Object obj1 = new Object();
Object obj2 = new Object();
synchronized (obj1) {
try{
obj2.wait();
}catch(InterruptedException e){
// different Object between `wait` and `synchronized`
e.printStackTrace();
}
}

这样限制的目的,是为了解决多线程编程下的一个问题:Lost Wake-Up Problem。

假设有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
// 初始
count=0;

// Thread-A
while(count<=0) {
  wait();
}
count --;

// Thread-B
count += 1;
notify();

考虑如下执行场景:

1

  • 线程A进行while判断,此时count=0,判断通过,准备进入循环内的代码;
  • 此时线程B开始执行,将count加一,并且发出了notify通知,但是由于线程A此时并没有wait,因此notify通知不会有线程响应;
  • 线程A恢复执行,执行wait方法,但是之后已经没有线程发出notify通知了。

上述问题便是“Lost wake-up problem”,为了防止这种情况发生,Java强制在调用wait和notify/notifyAll的时候,必须要在同步块中。

notify唤醒线程后,线程如何执行

  • notify线程

    当前线程发出notify之后,会继续执行代码,直到执行完synchronized代码块之后,才会释放锁。

    执行过程中如果遇到wait方法,则也会释放锁,进入WAITING状态。

  • wait线程

    wait线程收到notify之后被唤醒,但是唤醒之后不能立即从上次调用wait的地方恢复执行,因为之前中断的地方是在同步块里面,但是调用wait方法时,锁已经释放了,如果此时直接恢复执行,则违背了同步块的原则。

    因此,线程被唤醒之后,需要重新获取锁,此时有两种情况:

    • 没有出现锁竞争,获得锁成功,则线程进入RUNNABLE状态,从调用wait方法的地方恢复执行(reenter after calling Object.wait);
    • 出现了锁竞争,并且获得锁失败,则需要跟其他线程竞争锁,那么线程进入BLOCKED状态,等到获取锁之后,再从调用wait方法的地方恢复执行。

notify方法,会唤醒哪个线程?

如果有多个线程都进入了WAITING状态,那么调用notify,并不能保证会唤醒其中哪一个线程,这个是由JVM来决定的。

Spurious Wakeup(虚假唤醒)

虽然wait的定义是会等到notify调用之后,才会被唤醒,但是实际上存在一种虚假唤醒的情况,

一个线程可能会在没有被通知的情况下唤醒,虽然这点在实践中很少发生,但是建议编码时,将wait放在while循环判断中,这样唤醒之后若条件不满足,则会继续wait:

1
2
3
4
5
6
synchronized (obj) {
while (<condition does not hold>) {
obj.wait(timeout);
... // Perform action appropriate to condition
}
}

TIMED_WAITING

TIMED_WAITING是带有超时设置的WAITING版本,当线程调用如下方法时,会进入TIMED_WAITING状态:

  • Object.wait(long)方法(Object.wait with timeout);
  • Thread.join(long)方法(Thread.join with timeout);
  • LockSupport.parkNanos方法;
  • LockSupport.parkUntil方法;
  • Thread.sleep方法。

其他特征与WAITING保持一致。

Thread.sleep与Object.wait的区别

  • Object.wait方法必须要在同步块中才能调用,而Thread.sleep没有这个限制;
  • Object.wait方法会释放锁,而线程调用Thread.sleep时如果持有锁,那么在sleep期间不会释放锁。但是二者都会让出CPU;
  • 二者的唤醒机制不一样,前者只要sleep的时间到了就唤醒了;后者需要等待notify/notifyAll的调用。

TERMINATED

TERMINATED为终止状态,表示这个线程已经执行完毕。

参考



-=全文完=-