17 Java内存模型与线程_Java与线程
1 线程的实现
主流操作系统都提供线程的实现,在这基础上,上层应用可以构建自己的线程实现方式(Java、php、go的线程实现各不一样)。
三种线程实现方式:内核线程实现(1:1实现),用户线程实现(1:N实现), 用户线程加轻量级进程混合实现(N:M实现)
1.1 内核线程实现
内核线程:直接由操作系统内核支持的线程:
角色说明:
- 操作系统:内核、线程调度器、轻量级进程接口
- 内核:创建内核线程、创建轻量级进程(线程)
- 线程调度器:由内核控制,进行线程调度和任务派发
- 处理器:执行线程任务
- 内核线程:由内核创建管理
- 轻量级进程:由内核创建的,面向应用程序的线程。由内核线程一对一支持
工作流程:
- 应用程序通过轻量级进程接口,提交创建线程请求,连同任务内容传入内核
- 内核接收到线程创建请求,创建内核线程,同时创建面向应用程序的线程。线程与内核线程为1:1
- 内核控制线程调度器,将内核线程分派处理器,由处理器执行线程任务
应用:Java
1.2 用户线程实现
定义:完全建立在用户空间的线程库上,用户线程的建立、同步、销毁和调度完全在用户态中完成,不需要内核的帮助。
优点:不需要在内核态和用户态来回切换,因此快速且低消耗
缺点:线程的创建、销毁、切换和调度等操作都由用户实现,复杂
应用:Golang、Erlang等以高并发为卖点的编程语言,支持用户线程
1.3 用户线程加轻量级进程混合实现
定义:将内核线程与用户线程一起使用
优点:综合了两者的优点
应用:一些UNIX系列的操作系统,如Solaris、HP-UX
2 Java线程实现
采用:内核线程实现(1:1实现)
HotSpot:每个Java线程映射到一个操作系统原生线程来实现,虚拟机不会干涉线程调度(可以设置线程优先级给操作系统提供调度建议),全权交给操作系统去处理,包括:何时冻结或唤醒线程、该给线程分配多少cpu时间片、该把线程分给哪个处理器核心去执行。
注意:《Java虚拟机规范》没有限定线程采用什么模型来实现
3 Java线程调度
线程调度:系统为线程分配CPU使用权的过程,调度主要方式有两种,协同式线程调度 、抢占式线程调度
3.1 协同式线程调度
方式:执行时间由线程自身来控制,执行结束要主动通知系统切换到其它线程
优点:简单、不会有同步问题
缺点:一个线程有异常会导致系统停顿
应用:Lua语言中的“协同例程”
3.2 抢占式线程调度
方式:线程将由系统来分配执行时间,线程的切换不由线程本身来决定。
优点:不会因为一个线程异常导致系统停顿
应用:Java语言。
3.3 Java线程优先级
Java多线程环境下:
- 允许设置线程优先级给OS提供调度建议:处于Ready状态的线程,优先级越高的越容易被执行。
- Thread::yield()方法可以主动让出执行时间
- Java线程不能主动抢占执行时间 【只能让出】
设置线程优先级非完全可靠:
- OS可能会越过外部设置的优先级(Windows:“优先级推进器”:当系统发现一个线程被执行得特别频繁时,可能会越过线程优先级去为它分配执行时间)
- Java的线程优先级跟OS的线程优先级可能不匹配(Java10种,windows7种)
4 Java线程状态
六种状态:
-
新建(New):创建后尚未启动的线程处于这种状态
-
运行(Runnable):处于此状态的线程有可能正在执行,也有可能正在等待着操作系统为它分配执行时间。
-
无限期等待(Waiting):处于这种状态的线程不会被分配处理器执行时间,它们要等待被其他线程显式唤醒。触发情形:
- 没有设置Timeout参数的Object::wait()方法;
- 没有设置Timeout参数的Thread::join()方法;
- LockSupport::park()方法
-
限期等待(Timed Waiting):处于这种状态的线程也不会被分配处理器执行时间,不过无须等待被其他线程显式唤醒,在一定时间之后它们会由系统自动唤醒。触发情形:
- Thread::sleep()方法;
- 设置了Timeout参数的Object::wait()方法;
- 设置了Timeout参数的Thread::join()方法;
- LockSupport::parkNanos()方法;
- LockSupport::parkUntil()方法。
-
阻塞(Blocked):线程被阻塞了,“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。
-
结束(Terminated):线程已经结束执行结束
线程状态转换关系:
5 为什么内核线程调度切换成本更高?
5.1 成本在哪里
内核线程的调度成本主要来自于用户态与核心态之间的状态转换,而这两种状态转换的开销主要来自于响应中断、保护和恢复执行现场的成本
5.2 什么是上下文
- 程序员视角:方法调用过程中的各种局部的变量与资源
- 线程视角:方法的调用栈中存储的各类信息;
- 操作系统和硬件的视角:存储在内存、缓存和寄存器中的一个个具体数值
5.3 线程切换的成本分析
假设发生了这样一次线程切换:
线程A -> 系统中断 -> 线程B
成本分析:
- 已知1:程序的运行是代码+数据的组合,数据保存在“上下文”中
- 已知2:各种存储设备、寄存器是被操作系统内所有线程共享的资源
- 当中断发生,从线程A切换到线程B去执行之前,操作系统首先要把线程A的上下文数据妥善保管好,然后把寄存器、内存分页等恢复到线程B挂起时候的状态,线程B被重新激活并继续执行
- 保护和恢复现场的过程,涉及一系列数据在各种寄存器、缓存中的来回拷贝,这边便是成本所在
6 Java线程模型面临的困境
已知:Java采用1:1的内核线程模型
以前:单体应用,处理一个请求允许花费很长时间在单体应用中,线程数量少,线程切换的成本低
当下:微服务,服务数量多,线程数量也变多
矛盾:每个请求本身的执行时间变得很短、数量变得很多的前提下,用户线程切换的开销甚至可能会接近用于计算本身的开销,这就会造成严重的浪费。
7 学习收获
Java程序面向用户线程,操作系统管理内核线程,内核线程和用户线程1:1对应关系
保护和恢复现场的过程,涉及一系列数据在各种寄存器、缓存中的来回拷贝