转载自 深度好文 | Java 可重入锁内存可见性分析

一个习以为常的细节

之前在做 ReentrantLock 相关的试验,试验本身很简单,和本文相关的简化版如下:(提示:以下代码均可左右滑动)

private static ReentrantLock LOCK = new ReentrantLock();
private static int count = 0;
...
// 多线程 run 如下代码LOCK.lock();
try {count++;
} finally {LOCK.unlock();
}
...

就是通过可重入锁的保护并行对共享变量进行自增。

突然想到一个问题:共享变量 count 没有加 volatile 修饰,那么在并发自增的过程当中是如何保持内存立即可见的呢?上面的代码做自增肯定是没问题的,可见 LOCK 不仅仅保证了独占性,必定还有一种机制保证了内存可见性。

可能很多人和我一样,对 LOCK 的认知是如此 “理所应当”,以至于从没有去思考为什么。就好像每天太阳都会从东方升起而不觉得这有什么好质疑的。现在既然问到这儿了,那就准备一探究竟。

几个概念

Java Memory Model (JMM)

即 Java 内存模型,直接引用 wiki 定义:

"The Java memory model describes how threads in the Java programming language interact through memory. Together with the description of single-threaded execution of code, the memory model provides the semantics of the Java programming language."

JMM 定义了线程和内存之间底层交互的语义和规范,比如多线程对共享变量的写 / 读操作是如何互相影响。

Memory ordering

Memory ordering 跟处理器通过总线向内存发起读 (load)写 (store)的操作顺序有关。对于早期的 Intel386 处理器,保证了内存底层读写顺序和代码保持一致,我们称之为 program ordering,即代码中的内存读写顺序就限定了处理器和内存交互的顺序,所见即所得。而现代的处理器为了提升指令执行速度,在保证程序语义正确的前提下,允许编译器对指令进行重排序。也就是说这种指令重排序对于上层代码是感知不到的,我们称之为 processor ordering.

JMM 允许编译器在指令重排上自由发挥,除非程序员通过 synchronized/volatile/CAS 显式干预这种重排机制,建立起同步机制,保证多线程代码正确运行。

Happens-before

对于 volatile 关键字大家都比较熟悉,该关键字确保了被修饰变量的内存可见性。也就是线程 A 修改了 volatile 变量,那么线程 B 随后的读取一定是最新的值。然而对于如下代码有个很容易被忽视的点:

int a = 0;
volatile int b = 0;
...
a = 1; // line 1
b = 2; // line 2

当线程 A 执行完 line 2 之后,变量 a 的更新值也一同被更新到内存当中,JMM 能保证随后线程 B 读取到 b 后,一定能够看到 a = 1。之所以有这种机制是因为 JMM 定义了 happens-before 原则,直接贴资料:

  • Each action in a thread happens-before every action in that thread that comes later in the program order

  • An unlock on a monitor happens-before every subsequent lock on that same monitor

  • A write to a volatile field happens-before every subsequent read of that same volatile

  • A call to Thread.start() on a thread happens-before any actions in the started thread

  • All actions in a thread happen-before any other thread successfully returns from a Thread.join()on that thread

其中第 3 条就定义了 volatile 相关的 happens-before 原则,类比下面的同步机制,一图胜千言:

也就是说 volatile 写操作会把之前的共享变量更新一并发布出去,而不只是 volatile 变量本身。Happens-before 原则会保证 volatile 写之后,针对同一个 volatile 变量读,后面的所有共享变量都是可见的。

初步释疑

Happens-before 正是解释文章开头问题的关键,以公平锁为例,我们看看 ReentrantLock 获取锁 & 释放锁的关键代码:

private volatile int state; // 关键 volatile 变量
protected final int getState() {return state;
}// 获取锁
protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState(); // 重要!!!读 volatile 变量... // 竞争获取锁逻辑,省略
}// 释放锁
protected final boolean tryRelease(int releases) {boolean free = false;... // 根据状态判断是否成功释放,省略setState(c); // 重要!!!写 volatile 变量return free;
}

简单来说就是对于每一个进入到锁的临界区域的线程,都会做三件事情:

  • 获取锁,读取 volatile 变量;

  • 执行临界区代码,针对本文是对 count 做自增;

  • 写 volatile 变量 (即发布所有写操作),释放锁。

结合上面 happens-before 概念,那么 count 变量自然就对其它线程做到可见了。

事情还没有结束

我们只是利用 volatile 的 happens-before 原则对问题进行了初步的解释,happens-before 本身只是一个 JMM 的约束,然而在底层又是怎么实现的呢?这里又有一个重要的概念:内存屏障(Memory barriers)。

我们可以把内存屏障理解为 JMM 赖以建立的底层机制,wiki 定义:

"A memory barrier, also known as a membar, memory fence or fence instruction, is a type of barrier instruction that causes a central processing unit (CPU) or compiler to enforce an ordering constraint on memoryoperations issued before and after the barrier instruction. This typically means that operations issued prior to the barrier are guaranteed to be performed before operations issued after the barrier."

简而言之就是内存屏障限制死了屏障前后的内存操作顺序,抽象出来有四种内存屏障(因为内存 load/store 最多也就四种组合嘛),具体实现视不同的处理器而不同,我们这里看最常见的 x86 架构的处理器:volatile 的 happens-before 原则其实就是依赖的 StoreLoad 内存屏障,重点关注 LOCK 指令实现,这和 volatile 的底层实现息息相关,查看下面代码片段对应的汇编代码:

package main.java.happenbefore;
/**
* Created by zhoutong on 17/11/18.
*/
public class MyTest {private static long a;private static int b;private volatile static int c;public static void main(String[] args) {set();setAgain();}public static void set() {a = 1;b = 1;c = 1;}public static void setAgain(){if (c == 1) {a = 2;b = 2;}}
}

利用 hsdis 查看对应汇编片段(只包含关键部分):可以看到在 set() 方法对 a,b,c 赋值后,多出了一行 "lock addl $0x0,(%rsp)",这行代码只是对 stack pointer 加 0,无含义。但通过上文我们得知,x86 架构处理器的 LOCK prefix 指令相当于 StoreLoad 内存屏障。LOCK prefix 的指令会触发处理器做特殊的操作,查看 Intel 64 and IA-32 开发手册的相关资料:

"Synchronization mechanisms in multiple-processor systems may depend upon a strong memory-ordering model. Here, a program can use a locking instruction such as the XCHG instruction or the LOCK prefix to ensure that a read-modify-write operation on memory is carried out atomically. Locking operations typically operate like I/O operations in that they wait for all previous instructions to complete and for all buffered writes to drain to memory."

LOCK prefix 会触发 CPU 缓存回写到内存,而后通过 CPU 缓存一致性机制(这又是个很大的话题),使得其它处理器核心能够看到最新的共享变量,实现了共享变量对于所有 CPU 的可见性。

总结

针对本文开头提出的内存可见性问题,有着一系列的技术依赖关系才得以实现:count++ 可见性 → volatile 的 happens-before 原则 → volatile 底层 LOCK prefix 实现 → CPU 缓存一致性机制。

补充一下,针对 ReentrantLock 非公平锁的实现,相比公平锁只是在争夺锁的开始多了一步 CAS 操作,而 CAS 在 x86 多处理器架构中同样对应着 LOCK prefix 指令,因此在内存屏障上有着和 volatile 一样的效果。

参考资料

http://gee.cs.oswego.edu/dl/jmm/cookbook.html

https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.html 第 8 章

https://www.ibm.com/developerworks/library/j-jtp03304/index.html

https://stackoverflow.com/questions/2972389/why-is-compareandswap-instruction-considered-expensive

http://ifeve.com/java-memory-model-5/

https://en.wikipedia.org/wiki/Memory_barrier

https://en.wikipedia.org/wiki/Javamemorymodel

Java 可重入锁内存可见性分析相关推荐

  1. java可重入锁与不可重入锁

    所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的. synchronized 和   ReentrantLock 都是可重入锁. 可重 ...

  2. Java 可重入锁 不可重入锁

    文章目录 Java 可重入锁 & 不可重入锁 概述 论证synchronized是可重入锁: 论证Lock是可重入锁: 自定义不可重入锁: Java 可重入锁 & 不可重入锁 概述 可 ...

  3. Java 重入锁 ReentrantLock 原理分析

    1.简介 可重入锁ReentrantLock自 JDK 1.5 被引入,功能上与synchronized关键字类似.所谓的可重入是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生 ...

  4. java代码如何避免死锁,Java可重入锁如何避免死锁

    看到一个问题,Java的可重入锁为什么可以防止死锁呢?网上看了看资料,虽然有答案说出了正确答案,但是分析的不够详细,对初学者不够友好.这里我再做一个更清晰的分析. 首先是示例代码: 1 public ...

  5. java 可重入锁 clh_Java并发编程系列-(4) 显式锁与AQS

    4 显示锁和AQS 4.1 Lock接口 核心方法 Java在java.util.concurrent.locks包中提供了一系列的显示锁类,其中最基础的就是Lock接口,该接口提供了几个常见的锁相关 ...

  6. java 可重入锁 clh_Java可重入锁原理

    一. 概述 本文首先介绍Lock接口.ReentrantLock的类层次结构以及锁功能模板类AbstractQueuedSynchronizer的简单原理,然后通过分析ReentrantLock的lo ...

  7. JAVA可重入锁死锁

    可重入锁 可重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的. synchronized 和 ReentrantLock 都是可重入锁. ...

  8. Java多线程——重入锁ReentrantLock源码阅读

    上一章<AQS源码阅读>讲了AQS框架,这次讲讲它的应用类(注意不是子类实现,待会细讲). ReentrantLock,顾名思义重入锁,但什么是重入,这个锁到底是怎样的,我们来看看类的注解 ...

  9. java多线程---重入锁ReentrantLock

    1.定义 重入锁ReentrantLock,支持重入的锁,表示一个线程对资源的重复加锁. 2.底层实现 每个锁关联一个线程持有者和计数器,当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获 ...

最新文章

  1. PLSQ执行同样的sql,使用mybatis进行动态拼装执行的时候非常慢的问题解决
  2. Swift - 给图片添加文字水印(图片上写文字,并可设置位置和样式)
  3. 了解计算机中的信息编码教案,五年级下册信息技术《奇妙的编码》教学设计
  4. 【STM32】按键---有关GPIO输入的那些事
  5. 选择软件测试作为你的职业,一个无经验的大学毕业生,可以转行做软件测试吗?
  6. 嵌入式系统——面向对象的设计原则
  7. 题解报告:hdu 1575 Tr A
  8. android 可拖拽View的简单实现
  9. 程序员必知的Python陷阱与缺陷列表
  10. OO第四单元UML作业总结暨OO课程总结
  11. 看懂了这三个故事再结婚
  12. 锋利的jQuery总结(三)
  13. 【场景化解决方案】ERP系统与钉钉实现数据互通
  14. 服务器系统试用,“雪豹”安装篇(3)
  15. 本地idea通过tomcat启动服务停滞
  16. 清理谷歌浏览器注册表_Win10系统下注册表chrome残留无法删除
  17. cephfs:1 clients failing to respond to cache pressure原因分析
  18. 《安富莱嵌入式周报》第287期:下一代Windows12界面,支持各种工业以太网协议参考,百款在线电子开发工具,seL4安全微内核,旋转拨号手机,PSP掌机逆向
  19. 计算机管理服务报错mmc,mmc错误,注册表没有mmc
  20. Centos配置CA(证书颁发机构)

热门文章

  1. [C++11]不允许使用auto的四个场景
  2. 十一届蓝桥杯国赛 美丽的2-枚举
  3. Markdown编译器插入公式的数学符号及字体颜色、背景
  4. Seek the Name, Seek the Fame POJ - 2752 (理解KMP函数的失配)既是S的前缀又是S的后缀的子串
  5. 使用pdf.js来预览pdf文件_适用于Dynamics365与PowerApps的注释预览组件
  6. E - Flow Gym - 102471E
  7. I love exam HDU - 6968
  8. 2020牛客暑期多校训练营(第一场)
  9. Caddi Programming Contest 2021(AtCoder Beginner Contest 193) 题解
  10. AT3860-[AGC020F]Arcs on a Circle【dp】