《深入理解Java虚拟机》(第三版)读书笔记(九):第十二章 Java内存模型与线程

《深入理解Java虚拟机》(第三版)读书笔记(九):第十二章 Java内存模型与线程

Java内存模型

​ 《Java虚拟机规范》中曾试图定义一种“Java内存模型”(Java Memory Model,JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

主内存与工作内存

​ ​ ​ ​ ​ ​ Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节。

​ ​ ​ ​ ​ ​ Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程使用的变量的主内存副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据。线程间变量值的传递均需要通过主内存来完成。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TwUQOZz6-1579684490679)(E:\NayelyA\Documents\课程设计及学习资料\2020_winter\读书笔记\深入理解JVM\5.png)]

​ ​ ​ ​ ​ ​ 这里讲的主内存、工作内存和Java内存区域的Java堆、栈、方法区等并不是同一个层次的对内存的划分。

内存间交互操作

​ ​ ​ ​ ​ ​ 一个变量怎么从主内存拷贝到工作内存、如何从工作内存同步回主内存这一类的实现细节,Java内存模型中定义了以下8种操作完成,下面的这些操作都是原子的、不可再分的(对于double和long的变量,有些操作在某些平台上允许有例外)。

lock:作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

unlock:作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定。

read:作用于主内存的变量,把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。

load:作用于工作内存的变量,把read操作从主内存中得到的变量值放入到工作内存的变量副本中。

use:作用于工作内存的变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。

assign:作用于工作内存的变量,它把一个从执行引擎接受的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。

store:作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,方便write操作使用。

write:作用于主内存的变量,它把store操作从工作内存中的得到的变量的值放入主内存的变量里。

​ ​ ​ ​ ​ ​ 从主内存拷贝到工作内存,先read后load,从工作内存同步到主内存,先store然后write,这两个步骤的操作顺序不能颠倒,是否连续执行倒是无所谓。

​ ​ ​ ​ ​ ​ 一个变量在工作内存改变了必须得把改变同步回主内存,新变量只能在主内存里诞生,对一个变量进行unlock操作前,必须先把此变量同步回主内存里。

对于volatile型变量的特殊规则

​ ​ ​ ​ ​ ​ 关键字volatile是Java虚拟机提供的最轻量级的同步机制。如果一个变量被定义成volatile之后,将具备两项特性,第一项是保证此变量对所有线程的可见性(一条线程修改了该变量的值,新值对于其他线程来说是可以立刻得知的)。再说下普通变量是咋整的,线程A修改了一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再对主内存进行读取操作,新变量值才会对线程B可见。

​ ​ ​ ​ ​ ​ Java中的运算操作符并非原子操作,这导致volatile变量的运算在并发下一样是不安全的。如果不符合下面两条规则的运算场景中,我们还是需要加锁(使用synchronized、J.U.C中的锁或原子类)来保证原子性:

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值
  • 变量不需要于其他的状态变量共同参与不变约束

​ ​ ​ ​ ​ ​ 使用volatile除了可见性之外,还可以禁止重排序优化。普通变量仅保证在该方法的执行过程中所有以来赋值结果的地方都能获取到正确的结果,但不能保证变量赋值的顺序和程序代码中的执行顺序一致。

针对long和double型变量的特殊规则

​ ​ ​ ​ ​ ​ “long和double的非原子性协定“:允许虚拟机实现自行选择是否要保证64位数据类型的load、store、read和write四个操作的原子性。

原子性、可见性和有序性

原子性:基本数据类型(long和double例外)的访问、读写都是具备原子性的,如果应用场景需要一个更大范围的原子性保证,Java内存模型提供了lock和unlock操作满足需求,虚拟机提供字节码指令monitorenter和monitorexit隐式使用这两个操作,synchronized块之间的操作也具备原子性。

可见性:说volatile变量的时候解释过。普通变量和volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。synchronized和final也可以实现可见性。

有序性:如果在本线程内观察所有的操作都是有序的,如果在一个线程观察另一个线程,所有的操作都无序。volatile和synchronized可以保证有序性。

先行发生原则

​ ​ ​ ​ ​ ​ 先行发生是Java内存模型中定义的两项操作之间的偏序关系,比如操作A先行发生于操作B,也就是操作B发生前,操作A产生的影响(比如修改内存中共享变量的值、发送了消息、调用了方法)能被操作B观察到。

Java与线程
线程的实现

​ ​ ​ ​ ​ ​ 线程是比进程更轻量级的调度执行单位,线程可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源又可以独立调度。

​ ​ ​ ​ ​ ​ 实现线程主要有3种方式:使用内核线程实现(1:1实现),使用用户线程实现(1:N实现),使用用户线程加轻量级进程混合实现(N:M实现)。

1.内核线程实现

​ ​ ​ ​ ​ ​ 内核线程是直接由操作系统内核支持的线程,内核操纵调度器对线程调度,并将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核就成为多线程内核。

​ ​ ​ ​ ​ ​ 一般是使用内核线程的高级接口——轻量级进程,轻量级进程就是我们通常说的线程。由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。

​ ​ ​ ​ ​ ​ 轻量级进程的局限在于各种线程操作需要系统调用,然鹅系统调用的代价高,此外,轻量级进程消耗内核资源,系统支持轻量级进程数量有限。

2.用户线程实现

​ ​ ​ ​ ​ ​ 广义上说,一个线程只要不是内核线程,就可以认为是用户线程(UT),所以轻量级进程也算是用户线程,但是不具备通常意义上用户线程的优点,毕竟是建立在内核上的。

​ ​ ​ ​ ​ ​ 狭义上的用户线程建立在用户空间的线程库上,系统内核感知不到用户线程。优点是操作快速低消耗,能支持规模更大的线程数量,缺点是没有系统内核的支援,所有线程操作都得用户程序处理。

3.混合实现

​ ​ ​ ​ ​ ​ 既存在用户线程也存在轻量级线程。用户线程的创建、切换、析构等操作依然廉价,并且可以支持大规模的用户线程并发。使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级进程来完成,这大大降低了整个进程被完全阻塞的风险。

Java线程调度

​ ​ ​ ​ ​ ​ 线程调度是指系统为线程分配处理器使用权的过程,调度主要方式有两种,分别是协同式(Cooperative Threads-Scheduling)线程调度和抢占式(Preemptive Threads-Scheduling)线程调度

​ ​ ​ ​ ​ ​ 协同式多线程:线程的执行时间由线程本身来控制,线程把自己的工作执行完了后,要主动通知系统切换到另外一个县城上去。优缺点:实现简单,线程执行时间不可控。

​ ​ ​ ​ ​ ​ 抢占式线程调度:每个线程将有系统分配执行时间,线程的切换不由线程本身决定。线程的执行时间是系统可控的,Java使用的线程调度方式就是抢占式的。

​ ​ ​ ​ ​ ​ 虽然Java线程调度是系统自动完成的,但是仍可以建议操作系统给某些线程多分配执行时间,这是通过设置**线程优先级 **来完成的。不过,操作系统和Java线程的优先级未必能一一对应(数量级就不一样),除此之外,优先级可能会被系统改变。Windows系统中有一个叫优先级推进器的功能,作用是当系统发现一个线程被执行非常频繁的时候,可能会越过线程优先级去为它分配执行时间。所以在程序里通过优先级来完全准确判断一组状态都为Ready的线程将会先执行哪一个。

状态转换

​ 6种线程状态及转换关系图如下所示。
在这里插入图片描述
​ ​ ​ ​ ​ ​ 新建(New):创建和尚未启动的线程就处于该状态。

​ ​ ​ ​ ​ ​ 运行(Runnable):包括操作系统线程状态中的Running和Ready,也就是处于此状态的线程有可能正在执行,也可能正在等待着操作系统为其分配可执行时间。

​ ​ ​ ​ ​ ​ 无限期等待(Waiting):处于该状态的线程不会被分配执行时间,要等待被其他线程显式唤醒。没有设置Timeout参数的Object::wait()和Thread::join()方法和LockSupport::park()方法会导致陷入无限期等待。

​ ​ ​ ​ ​ ​ 限期等待(Timed Waiting):不会被分配处理器执行之间,不过无需等待显式唤醒,在一定时间后由系统自动唤醒。下面方法会让线程进入Timed Waiting:

​ ■Thread::sleep()方法;
​ ■设置了Timeout参数的Object::wait()方法;
​ ■设置了Timeout参数的Thread::join()方法;
​ ■LockSupport::parkNanos()方法;
​ ■LockSupport::parkUntil()方法。

​ ​ ​ ​ ​ ​ 阻塞(Blocked):“阻塞状态”与“等待状态”的区别是“阻塞状态”在等待着获取到一个排它锁,这个事件将在另外一个线程放弃这个锁的时候发生;而“等待状态”则是在等待一段时间,或者唤醒动作的发生。在程序等待进入同步区域的时候,线程将进入这种状态。

​ ​ ​ ​ ​ ​ 结束(Termminated):线程执行结束。

Java与协程

​ ​ ​ ​ ​ 有一说一,上次看面经,发现阿里的面试官问面试者是否了解协程。但是JVM这本书上写的还是不够详细,还得再看看别的。

内核线程的局限

​ ​ ​ ​ ​ ​ 1:1的内核线程模型是如今Java虚拟机线程实现的主流选择,但是这种映射到操作系统上的线程天然的缺陷是切换、调度成本高,而且前面我们也说了系统能容纳的线程数量也有限。但现在如果把数以百万计的请求往线程池里灌,即使系统可以处理的过来,切换内耗成本也是非常高的。书里面在协程的复苏这一小节还详细介绍了为什么内核线程调度切换起来成本更高的理由,在这儿不详细展开了,总结成一句就是,内核线程的切换开销是来自于保护和恢复现场的成本,如果改成用户线程,这部分开销也不会省略掉的。

协程的复苏

​ ​ ​ ​ ​ ​ 协程的优势是轻量,无论有栈协程还是无栈协程都比传统内核线程要轻量。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值