文章目录
今天复盘一下Java中,JVM线程与实际操作系统中线程的一些联系
请先思考下面问题:
JVM线程是用户态线程还是内核态线程
什么是用户态线程与内核态线程
先简单回顾下几个概念:
用户线程(User Thread):在用户空间实现的线程,不是由内核管理的线程,是由用户态的线程库来完成线程的管理;
内核线程(Kernel Thread):在内核中实现的线程,是由内核管理的线程
可能有朋友觉得,Java线程都是我们用户new出来的,应该是用户态吧?
这种感觉肯定是错的,因为用户也可以完成一些内核态操作,比如通过命令行来进行命令调用与shell脚本。
先说一下结论:早期Java的“绿色线程”是用户态线程,现在则是通过1:1线程映射模型映射到内核态线程。
绿色线程
在古早版本中(jdk1.2之前),Java的一个特大卖点就是跨平台特性,也就是那个经典口号:“Write once, run anywhere”。但那个时候有些平台还没提供本地线程的支持,无法将用户线程映射到OS线程。所以那会采用的是“绿色线程”(Green Threads),也就是在虚拟平台上模拟出“内核线程”。
而绿色线程运行在用户空间,通过第三方library或者VM进行调度,可以说属于用户态线程。
绿色线程的缺点
前面说了,绿色线程无法将用户线程映射为内核线程,只是在用户空间的模拟,对于造作系统来说都属于一个进程,仅有一个并发的概念,不能发挥多核CPU的优势去实现真正并行
线程映射
稍微回顾下线程映射模型
用户态线程是可以准备好程序让内核态线程执行,因此需要一些模型来实现用户线程和内核线程的对应关系:
- One to One:一个用户线程对应一个内核线程,一旦用户线程停止,两个线程都会离开OS
- 缺点:
- 操作系统限制了内核线程的数量
- 操作系统内核线程调度时,上下文切换的开销较大(相比于其他映射模型),导致用户线程的执行效率下降
- 缺点:
- Many to One:多个用户线程对应一个内核线程,用户线程间的切换由用户态代码实现
- 优点:相对一对一模型,多对一模型的线程切换速度要快许多,并且数量不受OS内核线程数的影响
- 缺点:
- 如果其中一个用户线程阻塞,时内核线程也随之阻塞,其他用户线程也无法执行
- 在多处理器系统上,处理器数量的增加对多对一模型的线程性能不会有明显的增加,因为所有的用户线程都映射到一个处理器上了
- Many to Many:多个用户线程对应到多个内核线程,内核线程数M可以小于用户态线程数N
- 优点:(解决了前两个的缺点)
- 一个用户线程的阻塞不会导致所有线程的阻塞
- 对用户线程的数量没有限制,
- 在多处理器的操作系统中,多对多模型的线程也能得到一定的性能提升,但提升的幅度不如一对一模型的高
- 优点:(解决了前两个的缺点)
JVM线程映射
由于绿色线程仅仅是线程概念的用户态模拟,不能并行,因此后来换成了映射的方式来将JVM线程映射到OS内核线程。
现在,每个Java线程都映射到操作系统中一个完全独立的线程(一般是One to One),其创建销毁等工作都通过内核操作完成。
线程状态
操作系统的线程状态
操作系统的线程可描述为五种状态:new、ready、run、block、end
JVM的线程状态
JVM线程可描述为六种状态,在Thread中有个枚举类型,即Thread.state:
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
- NEW:即线程刚创建,但未
start()
启动时的状态 - RUNNABLE:表示当前线程正在运行。此时有两种可能:
- 就绪状态:
start()
:启动线程,进入就绪状态,等待调度程序调度- 当前线程
sleep()
结束,或是其他线程join()
结束 yield()
出让线程
- 运行中状态:
- 线程调度程序从可运行池中选择一个线程作为当前线程时线程所处的状态(也是线程进入运行中状态的唯一方法)
- 就绪状态:
- BLOCKED:在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,阻塞等待其他线程释放锁
- WAITING:调用一些方法后会进入或设计等待状态,比如
Object.wait()
:使当前线程处于等待状态直到另一个线程通过notify()
显式唤醒它;Thread.join()
:插队,等待线程执行完毕,底层调用的是Object实例的wait方法;LockSupport.park()
:除非获得调用许可,否则禁用当前线程进行线程调度。- 处于等待状态的
- TIMED_WAITING:与WAITING类似,但是有指定的等待时间,比如:
Thread.sleep(long millis)
:使当前线程睡眠指定时间Object.wait(long timeout)
:线程休眠指定时间,等待期间可以通过notify()/notifyAll()唤醒;Thread.join(long millis)
:等待当前线程最多执行millis毫秒,如果millis为0,则会一直执行;- 上述三种方法使线程等待后,可以通过
Interrupt()
显示唤醒(中断等待)
- TERMINATED:run()方法完成时或者主线程main()方法完成时,线程就会关闭终止
- 在一个终止的线程上调用
start()
方法,会抛出java.lang.IllegalThreadStateException
异常
- 在一个终止的线程上调用
另外,我们可以通过创建线程的getState()
方法获取当前线程状态,或者感兴趣的朋友可以去了解下Jstack怎么做线程状态分析,可以用来排查死锁或者一些CPU占用过高的问题
JVM线程与OS线程的状态关系
Java线程的状态只会因为自身程序执行而发生转变,一般内核不会改变JVM线程状态
JVM线程与OS线程不一定是对应的,需要通过映射完成:
-
NEW 创建状态,对应OS线程的创建状态 new
-
TERMINATED 销毁状态,对应OS线程的销毁状态 end
-
RUNNABLE 运行状态,对应OS线程的run运行状态或ready就绪状态:只要操作系统正在运行,JVM就判定线程为run,并不一定对应内核线程的run状态。
- 有时,OS线程阻塞,单JVM看来OS仍在运行。比如Scanner.in与Scoket.accpet等待输入时,JVM线程还是run状态,而其映射的内核线程状态是wait
-
WAITING 等待状态,对应OS线程的wait状态:一般是由于调用了wait一类的方法陷入等待。
-
wait的作用-线程协作机制:
线程间并不都是竞争关系,也有协作关系。比如,一般一个进程中的多个线程除了普通线程外,还包括消耗线程、增加线程。消耗线程与增加线程间就是协作关系(生产者与消费者),所以需要wait,然后用notify特定唤醒,如果不特定唤醒,则会陷入线程的忙等待(比如说消耗线程没数据消费了,但是生产线程一直竞争不到执行权,没法生产新数据,消耗线程就一直等着)
注意一下,wait通过notify唤醒后并不会立即执行,而是跟其他线程一起重新竞争锁 资源
-
-
TIMED_WAITING 定时等待,对应OS线程的:相当于有限时间的wait,通过sleep等方式转变,通过interrupt打断。
-
sleep时会有锁的释放吗?
不会,sleep本身与并发关系不大,不会进行锁操作,此时如果有synchronized同步块,其他线程仍然不能访问共享数据。
-
-
BLOCKED 无法获取锁时的阻塞,就对应OS的block状态
- WAITING 与BLOCKED 的区别,一个是主动,一个是被动
Reference
https://www.runoob.com/java/thread-status.html
http://concurrent.redspider.group/article/01/4.html
https://www.bilibili.com/video/BV172421P7MB/?share_source=copy_web&vd_source=e40b707ba9b46ace5a15c44fb5fa3388
https://juejin.cn/post/7016228406220029983
https://www.zhihu.com/question/64100112/answer/218379442