深入理解Java虚拟机----第十二章:Java内存模型与线程

目录

第一章:走进Java
第二章:Java内存区域与内存溢出异常
第三章:垃圾收集器与内存分配策略
第四章:虚拟机性能监控与故障处理
第五章:调优案例分析与实战
第六章:类文件结构
第七章:虚拟机类加载机制
第八章:虚拟机字节码执行引
第九章:类加载及其执行子系统的案例与实战
第十章:早期(编译器)优化
第十一章:晚期(运行期)优化
第十二章:Java内存模型与线程
第十三章:线程安全与锁优化

第十二章:Java内存模型与线程

12.1概述

衡量一个服务端的好坏,每秒事物处理数(Transactions Per second,TPS)是最重要的指标之一

12.2硬件的效率与一致性

基于高速缓存的存储交互很好的解决了处理器与内存的速度矛盾,但是也为计算机系统带来了更高的复杂性,引入了新的问题:缓存一致性

为了使处理器内部运算单元尽量充分利用,处理器会对输入代码乱序执行优化

Java 虚拟机的即时编译器中也有类似的指令重排序优化

12.3 Java内存模型

Java虚拟机规范定义了一种内存模型(JAVA Memory Model,JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现Java语言在各个平台都能达到一致的内存访问效果

《深入理解Java虚拟机----第十二章:Java内存模型与线程》

12.3.1主内存和工作内存

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节,此处的变量和Java编程中所说的有所区别,它包括实例变量、静态字段、和构成数组对象的元素,但是不包括局部变量和方法参数,后者是线程私有的,不会被共享,自然就不会存在竞争问题

Java内存模型规定了所有的变量都存储在主内存(Main memory)中,每条线程还有自己的工作内存(Working Memory),线程的工作内存保存了被该线程使用的变量的主内存副本拷贝

线程对变量的操作(读取、赋值)必须都在工作内存中进行,而不能直接读写主内存中的变量

不同的线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递都通过主内存传递

12.3.2内存间交互操作

关于主内存与工作内存之间的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存,Java内存模型定义了8种操作来完成;每一种操作都是原子的、不可再分的(long和double有例外)

  • lock(锁定):作用于主内存变量,把一个变量状态标识一条线程独占的状态
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取):作用于主内存变量,把一个变量的值从主内存传输到工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存变量,把read操作从主内存得到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令的时候都会执行这个操作
  • assign(赋值):作用于工作内存,把执行引擎返回的结果值赋给工作内存的变量
  • store(存储):作用于工作内存的变量,把工作内存中的一个值传递到主内存中去,以便随后的write操作
  • write(写):作用于主内存变量,把store操作从工作内存中等到的变量值放到主内存的变量中去

执行8中操作时候必须满足的规则

  • 不允许read和load、store和write操作之一单独出现,即不允许一个变量从主内存读取了,在工作内存不接受,或者从工作内存发起回写,主内存不接受的情况
  • 不允许一个线程丢弃掉它最近的assign操作,即变量在工作内存改变以后必须把该变化同步到主内存
  • 不允许一个线程无原因的(没有发生过任何assign)把数据从工作内存同步到主内存
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过assign和load操作
  • 一个变量在同一时刻只能有一个线程对其进行lock操作,但是lock操作可以被同一线程重复执行多次,多次lock执行后,只有执行相同次数的unlock操作,变量才会被解锁
  • 如果对变量执行lock操作,将会清空工作内存此变量的值,在执行引擎使用这个变量钱前,需要重新load或者assign初始化变量的值
  • 如果一个变量事先没有被lock,就不允许执行unlock操作,也不允许unlock一个被其他线程lock的变量
  • 对一个变量unlock之前,必须把此变量同步到主内存

先行发生原则:用来确定一个访问在并发环境下是否安全

12.3.3对于volatile型变量的特殊规则

Java虚拟机提供的轻量级同步机制

作用:

  • 保证此变量对所有线程的可见性

不存在一致性问题

Java语言里的运算并非原子性,导致volatile变量的运算在并发下是安全的

在不符合以下两条规则的场景中,仍要通过加锁(使用synchronize或java.util.concurrent中的原子类)来保证原子性

运算结果并不依赖于变量的当前值,或者能够确保只有单一的线程修改变量的值

变量不需要与其他的状态变量共同参与不变约束

  • 禁止指令重排序优化

特殊规则

每次使用Volatitle变量前必须先从主内存刷新最新的值

工作内存中修改volatile变量后必须同步到主内存中

volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序和代码顺序一致

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

允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机不保证64位数据类型的load、store、read和write这4个操作的原子性,这就是long和double的非原子性协定

目前商业虚拟机不会出现读取到“半个变量”的情况

12.3.5原子性、可见性、有序性

原子性

  • 基本数据类型的访问读写是具备原子性(例外是long和double的非原子性协定)
  • synchronized块之间的操作具备原子性

可见性

当一个线程修改变量的值,其他线程能够立刻得知这个修改

synchronized、volatile、final都能保证可见性

有序性

如果在本线程内观察,所有的操作都是有序的,如果在一个线程中观察另一个线程,所有操作都是无序的

synchronized、volatile保证有序性

12.3.6先行发生原则

  • 程序次序规则:按程序代码顺序
  • 管程锁定规则:unlock优先于后面对同一个锁的lock操作
  • volatile变量规则:写优先于后面对这个变量的读操作
  • 线程启动规则:start()方法先行于每个动作
  • 线程终止规则:线程中所用操作先行于对此线程的终止检测
  • 线程中断规则:对线程的interrupt()方法的调用优先发生于被中断线程的代码检测到的中断事件的发生
  • 对象终结规则:初始化先行发生于finalize()
  • 传递性

12.4Java与线程

12.4.1线程的实现

3种方式:

  1. 使用内核线程实现

直接由操作系统内核支持的线程,由内核完成线程切换,内核通过操纵调度器进行线程调度,并且负责将线程的任务映射到各个处理器上

程序一般不会直接使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process ,LWP),轻量级进程就是我们通常意义上讲的线程

轻量级进程和内核线程之间1:1的关系称为一对一线程模型

《深入理解Java虚拟机----第十二章:Java内存模型与线程》

轻量级进程的局限性

  • 基于内核实现,各种线程操作需要进行系统调用,而系统调用的代价相对较高,需要在用户态(User Model)和内核态(Kernel Model)中来回切换
  • 每个轻量级进程都需要一个内核线程支持,轻量级进程要消耗一定的内核资源,因此一个系统支持轻量级进程的数量是有限的

    1. 使用用户线程实现

狭义的用户线程指的是完全建立在用户空间的线程库,系统内核不能感知线程存在

进程与线程之间的1:N的关系称为一对多的线程模型

优势在于不需要内核支援,劣势也在于没有内核支援

实现比较复杂,使用用户线程的程序越来越少

《深入理解Java虚拟机----第十二章:Java内存模型与线程》

  1. 使用用户线程加轻量级进程实现

用户线程完全建立在用户空间,用户线程的创建、切换、析构的操作比较廉价,并且支持大规模的用户线程并发

操作系统提供支持的轻量级进程则作为用户线程和内核线程的桥梁

使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低了整个进程被完全阻塞的风险

《深入理解Java虚拟机----第十二章:Java内存模型与线程》

12.4.2Java线程调度

线程调度是指系统为线程分配处理器使用权的过程

两种主要调度方式:

  1. 协同式线程调度

执行时间由线程本身决定,执行完后,主动通知系统切换到另一个线程

好处是实现简单;切换操作对线程自己可知,所以没有什么同步问题;

坏处是执行时间不可控;可能会出现程序一直阻塞的情况

  1. 抢占式线程调度

线程由系统分配执行时间,切换不由线程本身决定

执行时间系统可控,不会有一个线程导致整个进程阻塞的情况

Java使用的线程调度方式是抢占式线程调度

建议给系统分配时间:线程优先级

12.4.3状态转换

5种状态:

新建(New):创建后尚未启动

运行(Runable):包括操作系统线程状态中的Running和Ready,也就是正在执行和等待CPU为它分配执行时间的

无限期等待(Waiting):不会被CPU分配时间,要等待被其他线程显式地唤醒

  • 没有设置Timeout参数的Object.wait()
  • 没有设置Timeout参数的Thread.join()
  • LockSupport.park()

限期等待(Timed Waiting):不会被CPU分配时间,一定时间后会自动唤醒

  • Thread.sleep()
  • 设置Timeout参数的Object.wait()
  • 设置Timeout参数的Thread.join()
  • LockSupport.paNanos()
  • LockSupport.parkUntil

阻塞(Blocked):线程被阻塞

“阻塞状态”和“等待状态”的区别:

  • “阻塞状态”在等待一个排它锁
  • “等待状态”实在等待一段时间或者唤醒动作的发生

结束(Terminated):已终止的线程状态,线程已经执行结束
《深入理解Java虚拟机----第十二章:Java内存模型与线程》

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/qq_36969257/article/details/81502114
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞