17 Java内存模型与线程_Java与线程

1 线程的实现

主流操作系统都提供线程的实现,在这基础上,上层应用可以构建自己的线程实现方式(Java、php、go的线程实现各不一样)。
三种线程实现方式:内核线程实现(1:1实现),用户线程实现(1:N实现), 用户线程加轻量级进程混合实现(N:M实现)

1.1 内核线程实现

内核线程:直接由操作系统内核支持的线程:

角色说明:

  • 操作系统:内核、线程调度器、轻量级进程接口
  • 内核:创建内核线程、创建轻量级进程(线程)
  • 线程调度器:由内核控制,进行线程调度和任务派发
  • 处理器:执行线程任务
  • 内核线程:由内核创建管理
  • 轻量级进程:由内核创建的,面向应用程序的线程。由内核线程一对一支持

工作流程:

  1. 应用程序通过轻量级进程接口,提交创建线程请求,连同任务内容传入内核
  2. 内核接收到线程创建请求,创建内核线程,同时创建面向应用程序的线程。线程与内核线程为1:1
  3. 内核控制线程调度器,将内核线程分派处理器,由处理器执行线程任务

应用: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多线程环境下:

  1. 允许设置线程优先级给OS提供调度建议:处于Ready状态的线程,优先级越高的越容易被执行。
  2. Thread::yield()方法可以主动让出执行时间
  3. Java线程不能主动抢占执行时间 【只能让出】

设置线程优先级非完全可靠

  1. OS可能会越过外部设置的优先级(Windows:“优先级推进器”:当系统发现一个线程被执行得特别频繁时,可能会越过线程优先级去为它分配执行时间)
  2. 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. 已知1:程序的运行是代码+数据的组合,数据保存在“上下文”中
  2. 已知2:各种存储设备、寄存器是被操作系统内所有线程共享的资源
  3. 当中断发生,从线程A切换到线程B去执行之前,操作系统首先要把线程A的上下文数据妥善保管好,然后把寄存器、内存分页等恢复到线程B挂起时候的状态,线程B被重新激活并继续执行
  4. 保护和恢复现场的过程,涉及一系列数据在各种寄存器、缓存中的来回拷贝,这边便是成本所在

6 Java线程模型面临的困境

已知:Java采用1:1的内核线程模型

以前:单体应用,处理一个请求允许花费很长时间在单体应用中,线程数量少,线程切换的成本低

当下:微服务,服务数量多,线程数量也变多

矛盾:每个请求本身的执行时间变得很短、数量变得很多的前提下,用户线程切换的开销甚至可能会接近用于计算本身的开销,这就会造成严重的浪费。

7 学习收获

Java程序面向用户线程,操作系统管理内核线程,内核线程和用户线程1:1对应关系
保护和恢复现场的过程,涉及一系列数据在各种寄存器、缓存中的来回拷贝
posted @ 2022-12-15 20:25  拿了桔子跑-范德依彪  阅读(91)  评论(0编辑  收藏  举报