[Java]高效并发

  "深入理解Java虚拟机-线程安全与锁优化"

Posted by Stephen.Ri on May 16, 2020

一致性

在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(Main Memory)。

处理器、高速缓存、主内存之间的交互关系

为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。

Java内存模型

作用是屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

线程、主内存、工作内存关系

内存 存储内容 对应内容 物理硬件
主内存 所有的变量(包括实例字段,静态字段和构成数组对象的元素) Java堆中的对象实例数据 物理内存
工作内存 该线程使用到的变量的主内存副本拷贝 虚拟机栈中的部分区域 优先存储于寄存器和高速缓存中

内存间交互

Java内存模型定义了8种原子操作:

内存 发生位置 作用
lock 主内存 把一个变量标识为一条线程独占
unlock 主内存 释放锁定的变量
read 主内存 把一个变量的值从主内存传输到线程的工作内存
load 工作内存 把read操作从主内存中得到的变量值放入工作内存的变量副本
use 工作内存 把工作内存中一个变量的值传递给执行引擎
assign 工作内存 把一个从执行引擎接收到的值赋给工作内存的变量
store 工作内存 把工作内存中一个变量的值传送到主内存中
write 主内存 把store操作从工作内存中得到的变量的值放入主内存的变量

volatile特殊规则

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

  1. 保证变量对所有线程的可见性:当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的。

  2. 禁止指令重排序优化

先行发生原则

先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到。

Java内存模型中天然的先行发生原则:

  1. 程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作
  2. 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作
  3. volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作
  4. 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作
  5. 线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测
  6. 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  7. 对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始
  8. 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论

线程

线程的实现

线程是比进程更轻量级的调度执行单位,线程的引入,可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址、文件I/O等),又可以独立调度(线程是CPU调度的基本单位)。

线程 定义 模型 优点
内核线程 直接由操作系统内核支持的线程,由内核完成线程切换,线程调度,将线程任务映射到各处理器 一对一线程模型 程序可以使用高级接口——轻量级线程。实现简单
用户线程 完全建立在用户空间的线程库上,系统内核不能感知线程存在的实现 一对多线程模型 不需要切换到内核态,因此操作快速且低消耗,也可以支持规模更大的线程数量
混合实现 将内核线程与用户线程一起使用 多对多线程模型 轻量级进程则作为用户线程和内核线程之间的桥梁

对于Sun JDK来说,它的Windows版与Linux版都是使用一对一的线程模型实现的

线程调度

线程调度 定义 优点 Java
协同式调度 线程的执行时间由线程本身来控制,线程执行完后,要主动通知系统切换到另外一个线程上 实现简单,切换线程操作对自己可知 不使用
抢占式调度 每个线程将由系统来分配执行时间 线程的执行时间系统可控,不会有一个线程导致进程阻塞 使用

线程状态转换

线程状态转换关系

状态 定义 触发
新建 创建后尚未启动  
运行 包括操作系统中的Running和Ready  
无限期等待 等待被其他线程显式唤醒,不会被分配CPU执行时间 Object.wait(),Thread.join(),LockSupport.park()
限期等待 一定时间后,系统自动唤醒 Thread.sleep(),Object.wait(timeout),Thread.join(timeout),LockSupport.parkNanos(),LockSupport.parkUntil()
阻塞 等待着获取到一个排他锁  
结束 线程结束执行  

线程安全

线程安全定义

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的

Java中的线程安全

  1. 不可变的对象一定是线程安全的。如被final修饰的基本数据类型
  2. 绝对线程安全完全满足上述线程安全定义
  3. 相对线程安全,即通常意义所讲,需要保证这个对象单独的操作是线程安全的,调用的时候不需要做额外的保障
  4. 线程兼容,即对象本身不是线程安全的,但可以通过在调用端正确地使用同步手段来保证对象在并发环境下可以安全使用
  5. 线程对立,即无论调用端是否采用同步措施,都无法在多线程环境中并发使用

实现线程安全

互斥同步

同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,包括临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)等

  1. 最基本的互斥同步手段是synchronized。synchronized在编译后,会在同步块的前后分别形成monitorenter和monitorexit两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象的锁。synchronized是一个重量级操作。
  2. Reentrant Lock与synchronized很相似,他们都具备一样的线程重入特性。ReentrantLock增加了一些高级功能,主要有:等待可中断、可实现公平锁,以及锁可以绑定多个条件。

提倡在synchronized能实现需求的情况下,优先考虑使用synchronized来进行同步

非阻塞同步

基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施

CAS指令需要有3个操作数,分别是内存位置(在Java中可以简单理解为变量的内存地址,用V表示)、旧的预期值(用A表示)和新值(用B表示)。CAS指令执行时,当且仅当V符合旧预期值A时,处理器用新值B更新V的值,否则它就不执行更新

无同步方案

1.可重入代码:这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误 2.线程本地存储(Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,尝试保证这些共享数据的代码在同一个线程中执行。可以通过java.lang.ThreadLocal来实现线程本地存储

锁优化

虚拟机团队设计并实现各种锁优化技术

自旋锁

自旋锁:如果锁被占用时间很短,可以让后面请求锁的线程等待,即让线程执行一个忙循环(自旋),这样就可以避免线程切换的开销

自适应自旋锁:意味着等待时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定

锁消除

对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除

锁粗化

如果有一串零碎的操作都在对同一个对象加锁,将会把加锁同步的范围粗化到整个操作序列的外部

轻量级锁

在没有多线程竞争的前提下,通过CAS减少传统的重量级锁使用操作系统互斥量产生的性能消耗

偏向锁

在没有多线程竞争的情况下,把同步消除掉。这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步