1. 现代操作系统中的线程状态及转换(5种)2. Java 线程状态(6种)
2.1 NEW 创建2.2 RUNNABLE 运行2.3 BLOCKED 阻塞2.4 WAITING 等待2.5 TIMED_WAITING 超时等待2.6 TERMINATED 终止 3. Java 线程状态转换
3.1 BLOCKED 与 RUNNABLE 转换3.2 WAITING 与 RUNNABLE 转换3.3 TIMED_WAITING与RUNNABLE状态转换3.4 线程中断
在学习Java多线程时,总是被这么多种状态以及其转换方法搞得很头大,今天这篇文章就来捋一捋Java多线程中的几种线程状态以及相应的转换方法!
在讲解Java线程状态前先来看看在现代操作系统中更加主流的线程状态和转换:
1. 现代操作系统中的线程状态及转换(5种)在现在的操作系统中,线程是被视为轻量级进程的,所以操作系统线程的状态其实和操作系统进程的状态是一致的。具有以下 5 种状态:
创建态(new) :进程正在被创建,尚未到就绪状态。就绪态(ready) :进程已处于准备运行状态(等待被调度),即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。运行态(running) :进程正在处理器上上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。阻塞态(waiting) :又称为等待状态(等待资源),进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。结束/终止态(terminated) :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。
状态转换条件:
就绪态→运行态:处于就绪态的进程被调度后,获得处理机资源(分派处理机时间片),于是进程由就绪态转换为运行态。运行态→就绪态:处于运行态的进程在时间片用完后,不得不让出处理机,从而进程由运行态转换为就绪态。此外,在可剥夺的操作系统中,当有更高优先级的进程就绪时,调度程序将正在执行的进程转换为就绪态,让更高优先级的进程执行。运行态→阻塞态:进程请求某一资源(如外设)的使用和分配或等待某一事件的发生(如 I/O 操作的完成)时,它就从运行态转换为阻塞态。进程以系统调用的形式请求操作系统提供服务,这是一种由运行用户态程序调用操作系统内核过程的形式。阻塞态→就绪态:进程等待事件到来时,如 I/O 操作结束或中断结束时,中断处理程序必须把相应进程的状态由阻塞态转换为就绪态。
应该注意以下内容:
只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间片,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。 2. Java 线程状态(6种)
// Thread.State 源码
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
2.1 NEW 创建
处于 NEW 状态的线程此时尚未启动。这里的尚未启动指的是还没调用 Thread 实例的 start() 方法。
private void testStateNew() {
Thread thread = new Thread(() -> {});
System.out.println(thread.getState()); // 输出 NEW
}
从上面可以看出,只是创建了线程而并没有调用 start() 方法,此时线程处于 NEW 状态。
关于 start() 的两个引申问题:
- 反复调用同一个线程的 start() 方法是否可行?假如一个线程执行完毕(此时处于 TERMINATED 状态),再次调用这个线程的 start() 方法是否可行?
要分析这两个问题,先来看看 start() 的源码:
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}
可以看到,在 start() 内部,这里有一个 threadStatus 的变量。如果它不等于 0,调用 start() 是会直接抛出异常的。
接着往下看,有一个 native 的 start0() 方法。这个方法里并没有对 threadStatus 的处理。可以通过 debug 的方式再看一下:
@Test
public void testStartMethod() {
Thread thread = new Thread(() -> {});
thread.start(); // 第一次调用
thread.start(); // 第二次调用
}
在 start() 方法内部的最开始处打的断点,下面是在这里打断点看到的结果:
第一次调用时 threadStatus 的值是 0。第二次调用时 threadStatus 的值不为 0。
查看当前线程状态的源码:
// Thread.getState方法源码:
public State getState() {
// get current thread state
return sun.misc.VM.toThreadState(threadStatus);
}
// sun.misc.VM 源码:
public static State toThreadState(int var0) {
if ((var0 & 4) != 0) {
return State.RUNNABLE;
} else if ((var0 & 1024) != 0) {
return State.BLOCKED;
} else if ((var0 & 16) != 0) {
return State.WAITING;
} else if ((var0 & 32) != 0) {
return State.TIMED_WAITING;
} else if ((var0 & 2) != 0) {
return State.TERMINATED;
} else {
return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
}
}
所以,结合上面的源码可以得到引申的两个问题的结果:
2.2 RUNNABLE 运行两个问题的答案都是不可行,在调用一次 start() 之后,threadStatus 的值会改变(threadStatus !=0),此时再次调用 start() 方法会抛出 IllegalThreadStateException 异常。
比如,threadStatus 为 2 代表当前线程状态为TERMINATED。
表示当前线程正在运行中。处于 RUNNABLE 状态的线程可能在 Java 虚拟机中运行,也有可能在等待 CPU 分配资源。
Thread 源码里对 RUNNABLE 状态的定义:
2.3 BLOCKED 阻塞Java 线程的 RUNNABLE 状态其实是包括了传统操作系统线程的 就绪(ready) 和 运行(running) 两个状态的。
阻塞状态。处于 BLOCKED 状态的线程正等待锁的释放以进入同步区。
可以用 BLOCKED 状态举个生活中的例子:
2.4 WAITING 等待假如你正在银行窗口排队办理业务。你来到银行仅有的一个窗口,发现前面已经有个人在窗口前办理业务了,此时你必须得取一个号,等前面的人从窗口离开才行。
假设你是线程 t2,你前面的那个人是线程 t1。此时 t1 占有了锁(银行唯一的窗口),t2 正在等待锁的释放,所以此时 t2 就处于 BLOCKED 状态。
等待状态。处于等待状态的线程变成 RUNNABLE 状态需要其他线程唤醒。
调用如下 3 个方法会使线程进入等待状态:
Object.wait():使当前线程处于等待状态直到另一个线程唤醒它;Thread.join():使当前线程等待另一个线程执行完毕之后再继续执行,底层调用的是 Object 实例的 wait() 方法;LockSupport.park():除非获得调用许可,否则禁用当前线程进行线程调度。
延续上面的例子继续解释一下 WAITING 状态:
2.5 TIMED_WAITING 超时等待你等了好几分钟现在终于轮到你开始办理业务了,突然有一个 vip 客户来了。你看到他你就有一种不祥的预感,果然,他是来插队的(因为他是 vip,有优先特权无需取号)。
他把你挤到一旁让你等他完事了才能办业务。你心里虽然有一万个不愿意但是你还是拿着你的号从窗口走开了。
此时,假设你还是线程 t2,vip 客户是线程 t1。虽然你此时都占有锁(窗口)了,“不速之客”来了你还是得释放掉锁。此时你 t2 的状态就是 WAITING。然后 vip 客户 t1 获得锁,进入 RUNNABLE 状态。
要是 vip 客户不主动唤醒你 t2(通过 notify()、notifyAll() …),可以说你 t2 只能一直等待了。
超时等待状态。线程等待一个具体的时间,时间到后会被自动唤醒。
调用如下方法会使线程进入超时等待状态:
Thread.sleep(long millis):使当前线程睡眠指定时间,sleep() 方法不会释放当前锁,但会让出 CPU,所以其他不需要争夺锁的线程可以获取 CPU 执行;Object.wait(long timeout):线程休眠指定时间,等待期间可以通过 notify() / notifyAll() 唤醒;Thread.join(long millis):等待当前线程最多执行 millis 毫秒,如果 millis 为 0,则会一直执行;LockSupport.parkNanos(long nanos): 除非获得调用许可,否则禁用当前线程进行线程调度指定时间;LockSupport.parkUntil(long deadline):同上,也是禁止线程进行调度指定时间;
继续延续上面的例子来解释一下 TIMED_WAITING 状态:
2.6 TERMINATED 终止到了第二天中午,又有业务需要在银行办理,你还是到了窗口前。
突然间想起你的同事叫你等他一起,他说让你等他十分钟他整理一些资料就来。
好吧,你说那你就等等吧,你就离开了窗口。很快十分钟过去了,你见他还没来,你想都等了这么久了还不来,那你还是先去办理好手上的业务了。
这时你还是线程 t1,你的同事是线程 t2。t2 让 t1 等待了指定时间,此时 t1 等待期间就属于 TIMED_WATING 状态。
t1 等待 10 分钟后,就自动唤醒,拥有了去争夺锁的资格。
终止状态。此时线程已执行完毕。
3. Java 线程状态转换 3.1 BLOCKED 与 RUNNABLE 转换处于 BLOCKED 状态的线程是因为在等待锁的释放。假如有两个线程 a 和 b,a 线程提前获得了锁并且暂未释放锁,此时 b 就处于 BLOCKED 状态。先来看一个例子:
@Test
public void blockedTest() {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "a");
Thread b = new Thread(new Runnable() {
@Override
public void run() {
testMethod();
}
}, "b");
a.start();
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出?
System.out.println(b.getName() + ":" + b.getState()); // 输出?
}
// 同步方法争夺锁
private synchronized void testMethod() {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
初看之下,可能会觉得线程 a 会先调用同步方法,同步方法内又调用了 Thread.sleep() 方法,必然会输出TIMED_WAITING,而线程 b 因为等待线程 a 释放锁所以必然会输出 BLOCKED。
其实不然,有两点需要注意,一是在测试方法 blockedTest() 内还有一个 main 线程,二是启动线程后执行 run() 方法还是需要消耗一定时间的。
测试方法的 main 线程只保证了 a,b 两个线程调用 start() 方法(转化为 RUNNABLE 状态),如果 CPU 执行效率高一点,还没等两个线程真正开始争夺锁,就已经打印此时两个线程的状态(RUNNABLE)了。
当然,如果 CPU 执行效率低一点,其中某个线程也是可能打印出 BLOCKED 状态的(此时两个线程已经开始争夺锁了)。
那要是想要打印出 BLOCKED 状态该怎么处理呢?BLOCKED 状态的产生需要两个线程争夺锁才行。所以处理下测试方法里的 main 线程就可以了,让它“休息一会儿”,调用一下 Thread.sleep() 方法。
这里需要注意的是 main 线程休息的时间,要保证在线程争夺锁的时间内,不要等到前一个线程锁都释放了再去争夺锁,此时还是得不到 BLOCKED 状态的。
把上面的测试方法 blockedTest() 改动一下:
public void blockedTest() throws InterruptedException {
······
a.start();
Thread.sleep(1000L); // 需要注意这里main线程休眠了1000毫秒,而testMethod()里休眠了2000毫秒
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出?
System.out.println(b.getName() + ":" + b.getState()); // 输出?
}
在这个例子中两个线程的状态转换如下
a 的状态转换过程:RUNNABLE(a.start()) —> TIMED_WATING(Thread.sleep())—> RUNABLE(sleep() 时间到)—> BLOCKED(未抢到锁) —> TERMINATEDb 的状态转换过程:RUNNABLE(b.start()) —> BLOCKED(未抢到锁) —> TERMINATED
3.2 WAITING 与 RUNNABLE 转换斜体表示可能出现的状态,这里的输出也可能有多钟结果。
根据转换图可以知道有 3 个方法可以使线程从 RUNNABLE 状态转为 WAITING 状态。主要介绍下 Object.wait() 和 Thread.join()。
Object.wait()
调用 wait() 方法前线程必须持有对象的锁。
线程调用 wait() 方法时,会释放当前的锁,直到有其他线程调用 notify() / notifyAll() 方法唤醒等待锁的线程。
需要注意的是,其他线程调用 notify() 方法只会唤醒单个等待锁的线程,如有有多个线程都在等待这个锁的话不一定会唤醒到之前调用 wait() 方法的线程。
同样,调用 notifyAll() 方法唤醒所有等待锁的线程之后,也不一定会马上把时间片分给刚才放弃锁的那个线程,具体要看系统的调度。
Thread.join()
调用 join() 方法,会一直等待这个 Thread 线程执行完毕(转换为 TERMINATED 状态)再继续执行。
再把上面的例子线程启动那里改变一下:
public void blockedTest() {
······
a.start();
a.join();
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出 TERMINATED
System.out.println(b.getName() + ":" + b.getState());
}
要是没有调用 join() 方法,main 线程不管 a 线程是否执行完毕都会继续往下走。
a 线程启动之后马上调用了 a 线程的 join() 方法,这里 main 线程就会等到 a 线程执行完毕,所以这里 a 线程打印的状态固定是TERMINATED。
至于 b 线程的状态,有可能打印 RUNNABLE(尚未进入同步方法),也有可能打印 TIMED_WAITING(进入了同步方法)。
3.3 TIMED_WAITING与RUNNABLE状态转换TIMED_WAITING 与 WAITING 状态类似,只是 TIMED_WAITING 状态等待的时间是指定的。
Thread.sleep(long)
使当前线程睡眠指定时间。需要注意这里的“睡眠”只是暂时使线程停止执行,并不会释放锁。时间到后,线程会重新进入 RUNNABLE 状态。
Object.wait(long)
wait(long) 方法使线程进入 TIMED_WAITING 状态。这里的 wait(long) 方法与无参方法 wait() 相同的地方是,都可以通过其他线程调用 notify() 或 notifyAll() 方法来唤醒。
不同的地方是,有参方法 wait(long) 就算其他线程不来唤醒它,经过指定时间 long 之后它会自动唤醒,拥有去争夺锁的资格。
Thread.join(long)
join(long) 使当前线程执行指定时间,并且使线程进入 TIMED_WAITING 状态。
再来改一改刚才的示例:
public void blockedTest() {
······
a.start();
a.join(1000L);
b.start();
System.out.println(a.getName() + ":" + a.getState()); // 输出 TIEMD_WAITING
System.out.println(b.getName() + ":" + b.getState());
}
这里调用 a.join(1000L),因为是指定了具体 a 线程执行的时间的,并且执行时间是小于 a 线程 sleep 的时间,所以 a 线程状态输出 TIMED_WAITING。b 线程状态仍然不固定(RUNNABLE 或 BLOCKED)。
3.4 线程中断在某些情况下,我们在线程启动后发现并不需要它继续执行下去时,需要中断线程。目前在 Java 里还没有安全直接的方法来停止线程,但是 Java 提供了线程中断机制来处理需要中断线程的情况。
线程中断机制是一种协作机制。需要注意,通过中断操作并不能直接终止一个线程,而是通知需要被中断的线程自行处理。
简单介绍下 Thread 类里提供的关于线程中断的几个方法:
Thread.interrupt():中断线程。这里的中断线程并不会立即停止线程,而是设置线程的中断状态为true(默认是 flase);Thread.currentThread().isInterrupted():测试当前线程是否被中断。线程的中断状态受这个方法的影响,意思是调用一次使线程中断状态设置为 true,连续调用两次会使得这个线程的中断状态重新转为 false;Thread.isInterrupted():测试当前线程是否被中断。与上面方法不同的是调用这个方法并不会影响线程的中断状态。
在线程中断机制里,当其他线程通知需要被中断的线程后,线程中断的状态被设置为 true,但是具体被要求中断的线程要怎么处理,完全由被中断线程自己而定,可以合适地处理中断请求,也可以完全不处理继续执行下去。



