并发编程-底层实现原理
Java并发机制的底层原理总结如下:
1. volatile实现原理
(1)由该关键字声明的字段,Java线程内存模型确保所有线程看到这个变量的值是一致的。volatile变量的修改翻译成汇编语言为带LOCK前缀的指令,该指令会发生两件事:
① 将当前处理器缓存行的数据写回系统内存;
② 该写回内存的操作会使其他CPU里缓存了该内存地址的数据无效。从而保证变量的可见性。
(2)volatile具有两个特性:可见性、原子性和禁止指令重排序优化;
① 可见性,依托于读写happen-before关系,即当读取一个volatile变量时,JMM会把该线程对应的本地内存置为无效,从主内存中重新读取最后一次对该变量的写入值。
② 原子性,可以说是读写原子性,即对单个volatile变量的读写具有原子性,但不能保证volatile++这种复合操作具有原子性,如需做到原子性,需要添加锁实现。
③ 禁止指令重排序优化,当从源码翻译到执行指令序列时,会经过编译器优化重排序、指令级并行重排序、内存系统重排序等,在单线程程序中,这种重排序优化可以获得正确结果,但在多线程程序中,对存在控制依赖的操作重排序,可能会改变执行结果。禁止重排序,会在Java编译器生成指令序列的时候,插入特定类型的内存屏障指令,禁止volatile变量间及与普通变量间读写操作重排序。
JMM内存屏障的插入策略:
StoreStore屏障 | 在volatile写操作之前 | 禁止前边普通写与volatile写重排序 |
StoreLoad屏障 | 在volatile写操作之后 | 禁止后边volatile写与volatile读/写重排序 |
LoadLoad屏障 | 在volatile读操作之后 | 禁止后边普通读与volatile读重排序 |
LoadStore屏障 | 在volatile读操作之后 | 禁止后边普通写与volatile读重排序 |
2. synchronized实现原理
(1)利用Java中的每个对象作为锁实现同步,表现为三种形式:① 对普通同步方法,锁是当前实例对象;② 对静态同步方法,锁是当前类Class的对象;③ 对同步方法块,锁是synchronized括号中配置的对象。
(2)每个对象都有一个monitor与之关联,当且一个monitor被持有后,它处于锁定状态。在同步代码块中,编译后会插入monitorenter和monitorexit字节码指令,这两个字节码都需要一个reference类型的参数来指明锁定和解锁的对象,如果指定该对象,则reference即为该对象,如果没指定,需要根据synchronized修饰的是实例方法还是类方法,以确定是对象实例还是CLass对象。线程执行到monitorenter时,会尝试获取对象对应monitor的所有权。
(3)synchronized用的锁是存储在Java对象头里的。对象头中第一部分是Mark Word(存储对象的hash code或锁信息)。其中锁存在4个状态,级别从低到高:未锁定(锁标志为01)、可偏向(01)、轻量级锁定(00)和重量级锁状态(10),锁可以升级不可下降。
① 重量级锁:使用操作系统互斥量,对象头Mark Word 存有指向互斥量(重量级锁)的指针,线程竞争锁时,出现线程阻塞等待。之所以称为重量级锁,因为Java的线程是映射到操作系统的原生线程上的,如果阻塞或唤醒一个线程,都需要操作系统来帮忙完成,需要从用户态转换到核心态中,状态转换需要耗费很多处理器时间,可能比用户代码执行时间还长。
② 轻量级锁: 将对象头的Mark Word 复制到栈帧中的锁记录中(官方称为Displaced Mark Word),然后尝试使用CAS 将对象头的Mark Word 替换为指向锁记录的指针,如果成功,则表示没有竞争发生;如果失败,表示存在锁竞争,锁会膨胀为重量级锁。
③ 偏向锁:在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程再进入和退出同步块时,不需要进行CAS操作加锁和解锁。适用同一线程多次执行同步块。当存在另一个线程竞争时,偏向模式结束,根据锁对象目前是否处于锁定状态,撤销偏向后恢复为未锁定或轻量级锁定状态。
其中,轻量级锁和偏向锁是对重量级锁的优化,以提升同步性能,其依据为:对于绝大部分的锁,再整个同步周期内都是不存在竞争的。轻量级锁再无竞争情况下使用CAS操作去消除重量级使用的互斥量,偏向锁是在无竞争的情况下把整个同步都消除了,连CAS操作也不做了。
3. 原子操作实现原理
(1)在上一篇文章中,我们知道计算机硬件架构需要解决“缓存一致性”问题,处理器如何实现原子操作呢?基于缓存加锁或总线加锁,实现多处理器间的原子操作。
① 总线加锁:使用处理器提供一个LOCK#信号,当一个处理器在总线上输出次信号时,其他处理器的请求将被阻塞,由该处理器独占共享内存。但该方式开销比较大。
② 缓存加锁:利用缓存一致性机制(如上篇文章中说的MSI协议)来保证原子操作,其会组织同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定用的缓存行的数据时,会使缓存行无效,需要重新刷新数据。
(2)Java原子操作可以通过锁与CAS的方式
① 锁可以认为是通过互斥同步方式,可以通过API层面的互斥锁(ReentrantLock)和原生语法层面的synchronized;
② CAS(比较并交换,Compare-and-Swap),可以认为是非阻塞同步方式,或者说是基于冲突检测乐观并发策略。可能会引入ABA问题,J.U.C包提供一个带有标记的原子引用类AtomicStampedReference,通过控制变量值的版本来保证CAS的正确性。不过大部分情况下的ABA问题不会影响程序并发的正确性。