java可重入锁-ReentrantLock实现细节

  ReentrantLock支持两种获取锁的方式,一种是公平模型,一种是非公平模型。在继续之前,咱们先把故事元素转换为程序元素。

公平锁模型:

初始化时, state=0,表示无人抢占了打水权。这时候,村民A来打水(A线程请求锁),占了打水权,把state+1,如下所示:

  线程A取得了锁,把 state原子性+1,这时候state被改为1,A线程继续执行其他任务,然后来了村民B也想打水(线程B请求锁),线程B无法获取锁,生成节点进行排队,如下图所示:

  初始化的时候,会生成一个空的头节点,然后才是B线程节点,这时候,如果线程A又请求锁,是否需要排队?答案当然是否定的,否则就直接死锁了。当A再次请求锁,就相当于是打水期间,同一家人也来打水了,是有特权的,这时候的状态如下图所示:

  到了这里,相信大家应该明白了什么是可重入锁了吧。就是一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加。如果线程A释放了一次锁,就成这样了:

  仅仅是把状态值减了,只有线程A把此锁全部释放了,状态值减到0了,其他线程才有机会获取锁。当A把锁完全释放后,state恢复为0,然后会通知队列唤醒B线程节点,使B可以再次竞争锁。当然,如果B线程后面还有C线程,C线程继续休眠,除非B执行完了,通知了C线程。注意,当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除。

非公平锁模型:

  如果你已经明白了前面讲的公平锁模型,那么非公平锁模型也就非常容易理解了。当线程A执行完之后,要唤醒线程B是需要时间的,而且线程B醒来后还要再次竞争锁,所以如果在切换过程当中,来了一个线程C,那么线程C是有可能获取到锁的,如果C获取到了锁,B就只能继续乖乖休眠了。

实验代码

之前在做 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 获取锁 & 释放锁的关键代码:

rivate 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 一样的效果。

喜欢的小伙伴记得点点赞,也可以关注下我的专栏:

JAVA学习趣事​zhuanlan.zhihu.com

每天都有新鲜事!

java static 可见性_java可重入锁可见性分析相关推荐

  1. java中的账户冻结原理_java可重入锁(ReentrantLock)的实现原理

    前言 相信学过java的人都知道 synchronized 这个关键词,也知道它用于控制多线程对并发资源的安全访问,兴许,你还用过Lock相关的功能,但你可能从来没有想过java中的锁底层的机制是怎么 ...

  2. Java 并发编程之可重入锁 ReentrantLock

    Java 提供了另外一个可重入锁,ReentrantLock,位于 java.util.concurrent.locks 包下,可以替代 synchronized,并且提供了比 synchronize ...

  3. Java并发编程之ReentrantLock重入锁

    ReentrantLock: 源码层面分析: public class ReentrantLock implements Lock, java.io.Serializable {private sta ...

  4. Java成神之路——重入锁、公平非公平锁、自旋锁、读写锁

    你知道的Java锁有哪些? synchronized?Lock?它们又有什么区别?锁可分为哪些种类?锁是如何实现的? 公平与非公平锁 公平锁与非公平锁的区别体现在锁造成阻塞时的排队机制,公平锁按申请锁 ...

  5. Java并发编程-ReentrantLock可重入锁

    目录 1.ReentrantLock可重入锁概述 2.可重入 3.可打断 4.锁超时 5.公平锁 6.条件变量 Condition 1.ReentrantLock可重入锁概述 相对于 synchron ...

  6. 年轻人,看看 Redisson 分布式锁—可重入锁吧!太重要了

    作者 | 李祥    责编 | 张文 来源 | 企鹅杏仁技术站(ID:xingren-tech) 引言 作为后端开发,对于所谓的线程安全.高并发等一系列名词肯定都不会陌生,相关的一些概念及技术框架是面 ...

  7. 年轻人,看看Redisson分布式锁—可重入锁吧!太重要了

    1.引言 作为后端开发,对于所谓的线程安全.高并发等一系列名词肯定都不会陌生,相关的一些概念及技术框架是面试中的宠儿,也是工作中解决一些特定场景下的技术问题的银弹.今天我们就来聊聊这些银弹中的其中一枚 ...

  8. 可重入锁和不可重入锁

    转自https://www.cnblogs.com/dj3839/p/6580765.html 锁的简单应用 用lock来保证原子性(this.count++这段代码称为临界区) 什么是原子性,就是不 ...

  9. Java 可重入锁内存可见性分析

    转载自 深度好文 | Java 可重入锁内存可见性分析 一个习以为常的细节 之前在做 ReentrantLock 相关的试验,试验本身很简单,和本文相关的简化版如下:(提示:以下代码均可左右滑动) p ...

  10. java重入锁_java并发编程:可重入锁是什么?

    释义 广义上的可重入锁指的是可重复可递归调用的锁,在外层使用锁之后,在内层仍然可以使用,并且不发生死锁(前提得是同一个对象或者class),这样的锁就叫做可重入锁.ReentrantLock和sync ...

最新文章

  1. 六年级小学python第四讲_python第四讲
  2. 在C#中用COM操作CAD
  3. SQL基本语句及用法
  4. 分布式搜索ElasticSearch单机与服务器环境搭建
  5. runloop的mode作用是什么?
  6. tcp/ip网络协议学习
  7. 使用python自动玩游戏
  8. 简约竞聘个人简历自我介绍PPT模板
  9. 怎么批量修改html文件后缀,如何批量修改文件后缀名
  10. 视频教程-办公自动化,不求人!-Office/WPS
  11. pdf分解成多个pdf?PDF分割怎么做
  12. 搭建excel在线编辑服务器,开源免费!自动动手搭建一款更加强大的在线Excel工具...
  13. 自然辩证法2018版_自然辩证法-2018版课后思考题答案
  14. drain open 线与_Open-Drain与Push-Pull
  15. AI玩Flappy Bird│基于DQN的机器学习实例【完结】
  16. 先验概率、后验概率、似然函数概念的区分
  17. java xlsx转html
  18. IE6调试JS_se7en3_新浪博客
  19. 瓣膜赛道多方向,主要介入治疗
  20. 索尼WH-1000XM5什么时候发布 索尼WH-1000XM5配置怎么样

热门文章

  1. Html加水印和禁用复制和右键(jquery.watermark.js)
  2. Java clone() 浅拷贝 深拷贝
  3. 第二阶段冲刺(第十天)
  4. 截取一段字符串中,两个指定字符串中间的字符串
  5. C#版常用设计模式入门
  6. sqlite简单笔记
  7. 【带着canvas去流浪(13)】用Three.js制作简易的MARVEL片头动画(下)
  8. System.Timers.Timer(定时器)
  9. 【割点】【割边】tarjan
  10. python(3)-内置函数2