前言
线程并发系列文章:
Java 线程基础
Java “优雅”地中断线程
Java 线程状态
真正理解Java Volatile的妙用
Java ThreadLocal你之前了解的可能有误
Java Unsafe/CAS/LockSupport 应用与原理
Java 并发"锁"的本质(一步步实现锁)
Java Synchronized实现互斥之应用与源码初探
Java 对象头分析与使用(Synchronized相关)
Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
Java Synchronized 重量级锁原理深入剖析上(互斥篇)
Java Synchronized 重量级锁原理深入剖析下(同步篇)
Java并发之 AQS 深入解析(上)
Java并发之 AQS 深入解析(下)
Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
Java 并发之 ReentrantReadWriteLock 深入分析
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
最详细的图文解析Java各种锁(终极篇)
在现代操作系统里,有进程和线程的概念,我们先来简单了解一下这两个概念。
- 进程是资源分派的基本单位
- 线程是cpu调度基本单位
刚开始,计算机系统比较简单,设计者把cpu上执行的每个任务抽象为进程,操作系统统一管理进程的调度,给每个进程分配地址空间、外设等,这些统称为进程的资源。当cpu执行A进程过程中,发现A的时间片已经耗尽,因此先将A进程挂起(释放cpu),并保存进程A的上下文,然后再调度进程B。后来发现每次切换进程的上下文都比较耗时,浪费有限的系统资源,因此将进程再划分为粒度更小的单位,称之为线程(实际上是cpu上的一个执行流),进程内的线程共享进程的资源,因此cpu切换线程所花代价更小。
Linux进程状态
cpu同一时刻只能处理一个进程,有可能进程还在排队等待cpu执行,可能在某个地方等待资源(如输入事件等),因此操作系统给予进程状态来标识进程某个时刻的状态。[1]
如上图,进程状态为ready(就绪),running(运行中),waiting(等待)
实际上线程状态也可以类比进程状态。
Java线程状态
Java是门支持多线程的语言,Java线程是JVM实现的,JVM是OS的一个进程,Java线程对于OS来说是透明的。接下来我们就Java线程类Thread入手了解一下Java线程状态。
public enum State {
/**
* Thread state for a thread which has not yet started.
*/
NEW,
/**
* Thread state for a runnable thread. A thread in the runnable
* state is executing in the Java virtual machine but it may
* be waiting for other resources from the operating system
* such as processor.
*/
RUNNABLE,
/**
* Thread state for a thread blocked waiting for a monitor lock.
* A thread in the blocked state is waiting for a monitor lock
* to enter a synchronized block/method or
* reenter a synchronized block/method after calling
* {@link Object#wait() Object.wait}.
*/
BLOCKED,
/**
* Thread state for a waiting thread.
* A thread is in the waiting state due to calling one of the
* following methods:
*
* - {@link Object#wait() Object.wait} with no timeout
* - {@link #join() Thread.join} with no timeout
* - {@link LockSupport#park() LockSupport.park}
*
*
* A thread in the waiting state is waiting for another thread to
* perform a particular action.
*
* For example, a thread that has called Object.wait()
* on an object is waiting for another thread to call
* Object.notify() or Object.notifyAll() on
* that object. A thread that has called Thread.join()
* is waiting for a specified thread to terminate.
*/
WAITING,
/**
* Thread state for a waiting thread with a specified waiting time.
* A thread is in the timed waiting state due to calling one of
* the following methods with a specified positive waiting time:
*
* - {@link #sleep Thread.sleep}
* - {@link Object#wait(long) Object.wait} with timeout
* - {@link #join(long) Thread.join} with timeout
* - {@link LockSupport#parkNanos LockSupport.parkNanos}
* - {@link LockSupport#parkUntil LockSupport.parkUntil}
*
*/
TIMED_WAITING,
/**
* Thread state for a terminated thread.
* The thread has completed execution.
*/
TERMINATED;
}
注释比较清晰,这6种状态涵盖了Thread整个生命周期,简单概括一下:
1、NEW:构造了thread实例,但是还没有start
2、RUNNABLE:线程正在运行或者正等待被cpu执行
3、BLOCKED:线程调用synchronized关键字等待获取monitor锁
4、WAITING:线程调用了无超时的wait、join、park方法
5、TIMED_WAITING:线程调用了有超时的wait、sleep、join、parkNanos、parkUntil方法
6、TERMINATED:线程终止/完成了运行
线程各种状态之间是如何转换的呢?用图说明:[2]
这张图清晰展示了各个状态之间的关系,大家可能注意到图上多了两个状态:Ready和Running,而少了RUNNABLE状态。还记得之前说的RUNNABLE:“线程正在运行或者正等待被cpu执行”,实际上RUNNABLE可以认为是Ready和Running状态的结合体,Ready表示就绪状态,Running表示正在运行的状态。
代码验证状态
上面的状态描述可能比较抽象,Java 1.5之后有个方法:getState() 用来获取线程的状态,因此我们可以打印该值来观察状态之间的流转。
验证BLOCKED状态
public class TestThreadState {
public static void main(String args[]) {
final Object object = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("t1 getted lock");
while(true);
}
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("t2 getted lock");
}
}
}, "t2");
//test state
t1.start();
sleep(2);
t2.start();
while(true) {
System.out.println("t1 state:" + t1.getState());
System.out.println("t2 state:" + t2.getState());
sleep(2);
}
}
private static void sleep(long time) {
try {
TimeUnit.SECONDS.sleep(time);
} catch (Exception e) {
}
}
}
t1先获得monitor lock,并一直循环不释放锁,此时状态为RUNNABLE;t2尝试获取monitor lock失败,并阻塞,此时状态为BLOCKED。
需要注意的是,如果使用ReentrantLock,线程因ReentrantLock阻塞,此时线程状态为WAITING。
验证WAITING/TIMED_WAITING状态
将上面的例子稍微修改为:
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("t1 getted lock");
try {
object.wait();
System.out.println("t1 get signal from t2");
} catch (Exception e) {
}
}
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
System.out.println("t2 getted lock");
sleep(3);
System.out.println("t2 notify");
try {
object.notify();
} catch (Exception e) {
}
//先睡眠
sleep(3);
}
}
}, "t2");
t1先获得锁,并调用wait阻塞线程和放弃锁,此时状态为:WAITING。t2获取锁成功,并睡眠3s,此时状态为:TIMED_WAITING。t2睡眠结束后调用notify唤醒t1,此时t2继续睡眠,状态为:TIMED_WAITING。t1收到notify信息后,尝试获取锁,但由于此时t2还在睡眠没有释放锁,因此t1被阻塞,状态为BlOCKED。t2睡眠结束,释放锁,t1获得锁并接着打印“t1 get signal from t2”语句,最终t1、t2终止了运行,处在TERMINATED状态。
Join和Yield
Join是Thread类成员方法,该方法作用是当前线程等待某个线程结束后再继续执行后续代码。
Yield是Thread类静态方法,该方法作用是让当前线程放弃cpu,并等待cpu重新调度(当前线程可能再次获得cpu)。通俗点的说法是给其它线程一个机会,让大家回到同一起跑线,至于谁被cpu选中,看个人造化。
public class TestJoin {
public static void main(String args[]) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000);
System.out.println("t1 end");
} catch (Exception e) {
}
}
}, "t1");
t1.start();
try {
System.out.println("wait t1 terminate");
t1.join();
} catch (Exception e) {
}
System.out.println("main thread end");
}
}
main thread等待t1执行完成再继续执行。
网上不少文章列举Yield用法示例:
public class TestYield {
public static void main(String args[]) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
if (i == 3) {
Thread.yield();
}
System.out.println("t1->" + i);
}
}
}, "t1");
t1.start();
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("t2->" + i);
}
}
}, "t2");
t2.start();
}
}
t1在执行期间调用yield()方法,放弃cpu,这时候t2就可以拿到cpu。实际上打印结果的顺序充满不确定性,不能完全确定是由于yield()方法引起的差异,因此上面的例子不够充分。而我们平时使用yield()方法的情况很少,基本无需关注。
LockSupport
用于线程阻塞与唤醒,更详细的解释请查看:Java Unsafe/CAS/LockSupport 应用与原理
线程状态的意义
在开发的过程中,免不了用到多线程。多线程之间的协作(同步、互斥)需要正确有序的进行,在程序运行过程中如果出现了多线程问题一般都比较随机不容易复现(死锁等),这个时候可以通过打印线程状态(jstack)来分析各个线程状态,到底处在什么状态?因为什么原因处在这个状态?实际应该进行到哪个状态?这样对于问题的分析就会做到有的放矢,提高效率。
对如何中断线程感兴趣的同学请移步:Java “优雅”地中断线程
参考文章
[1] https://www.tecmint.com/linux-process-management/
[2] https://www.uml-diagrams.org/examples/java-6-thread-state-machine-diagram-example.html