Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >锁的实现原理解锁的实现加锁的实现

锁的实现原理解锁的实现加锁的实现

作者头像
用户1174983
发布于 2018-02-05 07:35:55
发布于 2018-02-05 07:35:55
1.5K03
代码可运行
举报
文章被收录于专栏:钟绍威的专栏钟绍威的专栏
运行总次数:3
代码可运行

 锁在多线程中是必不可少的,他给多线程提供了同步的功能,让多线程可以互斥的执行同步块,并具有可见性

 本文将从happens-before关系出发,结合ReentranLock源码,如何用内存屏障、CAS操作、LOCK指令实现锁的功能。

锁的happens-before关系

happens-before规则

  1. 程序顺序规则:在一个线程中,前面的操作happens-before后面的操作
  2. 锁规则:对同一个锁,解锁happens-before加锁。
  3. 传递性规则:A happens-before B,B happens-before C,则A happens-before C

 从这段代码看看happens-before关系,线程A先执行store(),线程B后执行load()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int value = 0;
boolean finish = 0;

//线程A
void store(){
    //A:加锁前的操作
    synchronized(this){ //B:加锁
        value = 1;      //C:写value
        finish = true;  //D:写finish
    }                   //E:解锁
    //F:解锁后的操作
}

//线程B
void load(){
    //G:加锁前的操作
    synchronized(this){ //H:加锁
        if(finish){     //I:读finish
            assert value == 1; //J:读value
        }
    }                   //K:解锁
    //L:解锁后的操作
}

 这里有13个happens-before关系。①~⑤是线程A的程序顺序关系,⑥~⑩是线程B的程序顺序关系,⑪是锁规则关系,⑫~⑬是传递性关系

从happens-before关系分析可见性

①~⑩根据程序顺序规则,只要不重排序数据依赖的指令,执行结果就是正确的,就可以保证在单线程内的可见性。

根据锁规则,E happens-before H,也就是线程A解锁 happens-before 线程B加锁

根据传递性规则,线程A解锁前的操作都需要对线程B加锁可见,ABCDE happens-before H,也就是线程A解锁及其先前操作 happens-before 线程B加锁

再根据传递性规则,线程A解锁前的操作都需要对线程B加锁之后的操作可见,ABCDE happens-before HIJKL,最终得出线程A解锁及其先前操作 happens-before 线程B加锁及其后续操作

 这样来看,为了保证解锁及其之前操作的可见性,需要把解锁线程的本地内存刷新到主内存去。同时为了保证加锁线程读到最新的值,需要将本地内存的共享变量设为无效,重新从主内存中读取。

实现锁的原理

前面得出来的锁的可见性:线程A解锁及其先前操作 happens-before 线程B加锁及其后续操作

 将前面得出的可见性分解为三个等级:

  1. 线程A解锁 happens-before 线程B加锁
  2. 线程A解锁及其先前操作 happens-before 线程B加锁
  3. 线程A解锁及其先前操作 happens-before 线程B加锁及其后续操作

由于这是在多线程间实现可见性,那么就要考虑本地内存和主内存的缓存不一致问题,需要用到JMM的内存屏障:

 逐级的实现可见性:

 1) 对于第一级可见性,线程A解锁 需要对 线程B加锁可见,在多线程间的,会引发缓存不一致,所以要把线程A的本地内存刷新到主内存去。所以在解锁、加锁之间需要加写读内存屏障,这里有两种实现方式:

  1. 在线程A解锁后加StoreLoad Barrier
  2. 在线程B加锁前,加StoreLoad Barrier。

 在常用的开发模式中,常常是一个线程负责写,多个线程负责读,典型的像生产者-消费者模式。所以相较后者,前者的内存屏障执行次数少,性能高。采用第一种实现方式比较好。

 2) 对于第二级可见性,线程A解锁前的操作需要对加锁可见,也就是线程A解锁前的操作不能被重排序到解锁后。由于只有写操作会对改变共享变量,所以需要在解锁前加上StoreStore Barrier

 3) 对于第三级可见性,线程B加锁之后的读写操作不能重排序到加锁前,否则线程B可能读不到线程A的操作结果,以及线程B可能在线程A之前修改了共享变量。所以需要在线程B加锁后加上LoadLoad Barrier 和 LoadStore Barrier

 综上所述:

  1. 解锁前加StoreStore Barrier
  2. 解锁后加StoreLoad Barrier
  3. 加锁后加LoadLoad Barrier 和LoadStore Barrier

 加上内存屏障后的程序:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int value = 0;
boolean finish = 0;

//线程A
void store(){
    //A:加锁前的操作
    synchronized(this){ //B:加锁
        loadLoadBarrier();
        loadStoreBarrier();
        value = 1;      //C:写value
        finish = true;  //D:写finish
        storeStoreBarrier();
                        //E:解锁
        storeLoadBarrier();
    }                   
    //F:解锁后的操作
}

//线程B
void load(){
    //G:加锁前的操作
    synchronized(this){ //H:加锁
        loadLoadBarrier();
        loadStoreBarrier();
        if(finish){     //I:读finish
            assert value == 1; //J:读value
        }
        storeStoreBarrier();
                        //K:解锁
        storeLoadBarrier();
    }
    //L:解锁后的操作
}

分析锁的源码

Java提供的锁可以分为两种:隐形锁和显性锁。隐形锁就是常用的synchronized语句,是由Java语法提供的,语法的源码比较难找。在这里用显性锁的源码去分析,显性锁实际上是Java中的一个工具类,允许以调用函数的形式去加锁解锁。从功能上看显性锁的功能更强大,因为其能通过继承实现不同算法的锁,以便根据实际情况选择合适的锁。这里使用ReentrantLock去分析源码。

 在前面实现锁的原理中,得出实现可见性的原理是在加锁解锁前后加上内存屏障。乍一看这不是和volatile的原理是一模一样的吗,连使用的内存屏障种类顺序都一样。所以在ReentrantLock中,他复用了volatile提供的可见性,并没有再去写内存屏障。

 在ReentrantLock中,他有一个变量state是volatile的(继承自AbstractQueuedSynchorinizer)。解锁-加锁分别是由写-读state这个volatile变量去实现的。这个state变量可以理解成所被重入的次数(ReentrantLock是可重入锁),0表示没有线程拥有该锁,2表示被拥有者连续拥有了两次且没有释放。

 ReentranLoack分为公平锁和不公平锁,下面分别看看这两种锁在解锁加锁的源码。

解锁的实现

 公平锁和不公平锁的对于解锁的实现都是一样的,都是写state变量。最后都是调用ReentranLock.Sync.tryRelease()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//在java.util.concurrent.locks.ReentranLock.Sync.tryRelease()
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())//如果当前线程不是该锁的拥有者则抛出异常
        throw new IllegalMonitorStateException();
    boolean free = false;//锁是否可用
    if (c == 0) {//state=0 表示该持有线程完全释放该锁,需要设置free为可用状态以及拥有者线程置空
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);//在释放锁的最后,写state
    return free;
}

 根据volatile原理知道,写state这个volatile变量也就相当于

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
storeStoreBarrier();
解锁;
storeLoadBarrier();

 这样的内存屏障和前面锁原理分析的是一样的,所以写volatile与解锁有一样的功能,也就能使用写volatile的方式实现解锁

加锁的实现

 加锁中,公平锁和不公平锁实现的方式就有很大的不同了。公平锁使用的是读volatile,不公平锁使用的是CompareAndSet(CAS)

公平锁的加锁实现

 先看公平锁的读state加锁实现,核心代码在ReentranLock.FairSync.tryAcquire()。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//在java.util.concurrent.locks.ReentranLock.FairSync.tryAcquire()
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();//在加锁的一开始,读state
    if (c == 0) {//锁处于可用状态
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);//设置锁被当前线程拥有
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {//state>0,重入了
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");//超过最大重入次数2147483648(最大的int)
        setState(nextc);//更新state
        return true;
    }
    return false;
}

 根据volatile原理知道,读state这个volatile变量也就相当于

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
加锁;
loadLoadBarrier();
loadStoreBarrier();

 这样的内存屏障和前面锁原理分析的是一样的,所以读volatile与加锁有一样的功能,也就能使用读volatile的方式实现加锁

不公平锁的加锁实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//在java.util.concurrent.locks.ReentranLock.NoFairSync.lock()
final void lock() {
    if (compareAndSetState(0, 1))//如果该锁可用,则占有
        setExclusiveOwnerThread(Thread.currentThread());
    else//尝试重入
        acquire(1);
}
//在java.util.concurrent.locks.AbstractQueuedSynchronizer.compareAndSetState()
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

 如果该锁没占用的时候,调用的是unsafe.compareAndSwapInt(),这是一个CAS操作。如果该锁已经被占有了,尝试重入,这部分的代码是使用和公平锁一样的读state方式实现的。

 unsafe.compareAndSwapInt()这是一个native方法,是用JNI调用C++或者汇编的,需要到openjdk看,位置在:openjdk-7-fcs-src-b147- 27_jun_2011\openjdk\hotspot\src\os_cpu\windows_x86\vm\atomic_windows_x86.inline.hpp

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//CAS源码:
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest,
        jint compare_value) {
        // alternative for InterlockedCompareExchange
    int mp = os::is_MP();//是否为多核心处理器
    __asm {
        mov edx, dest           //要修改的地址,也就是state变量
        mov ecx, exchange_value //新值值
        mov eax, compare_value  //期待值
        LOCK_IF_MP(mp)          //如果是多处理器,在下面指令前加上LOCK前缀
        cmpxchg dword ptr [edx], ecx//[edx]与eax对比,相同则[edx]=ecx,否则不操作
    }
}

 这里看到有一个LOCK_IF_MP,作用是如果是多处理器,在指令前加上LOCK前缀,因为在单处理器中,是不会存在缓存不一致的问题的,所有线程都在一个CPU上跑,使用同一个缓存区,也就不存在本地内存与主内存不一致的问题,不会造成可见性问题。然而在多核处理器中,共享内存需要从写缓存中刷新到主内存中去,并遵循缓存一致性协议通知其他处理器更新缓存。 Lock在这里的作用:

  1. 在cmpxchg执行期间,锁住内存地址[edx],其他处理器不能访问该内存,保证原子性。即使是在32位机器上修改64位的内存也可以保证原子性。
  2. 将本处理器上写缓存全部强制写回主存中去,也就是写屏障,保证每个线程的本地内存与主存一致。
  3. 禁止cmpxchg与前后任何指令重排序,防止指令重排序。

 可见CAS操作具有与读写volatile变量一致的作用,都能保证可见性。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2017-12-25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Volatile实现原理实现原子性happens-before关系从happends-before规则分析可见性编译器层面实现可见性处理器层面实现可见性
 读写volatile变量就像是访问一个同步块一样,是原子的且是可见的,总是能访问到最新的值。 原子性  读写volatile变量是原子操作,但读写变量不就是一条指令的事吗(mov、ldr),难道这还可分?没错绝大多数变量读写都是原子的,除了在32位JVM下对long、double的读写,就不是原子的。这是因为在32位下,总线宽度就只有32bit,对64位数据的读写需要分两次进行,依次读写高低32位。但是读写volatile变量由于使用了LOCK前缀指令,锁住了内存,所以即使是64位的数据也是原子的。 读
用户1174983
2018/02/05
1.9K0
Volatile实现原理实现原子性happens-before关系从happends-before规则分析可见性编译器层面实现可见性处理器层面实现可见性
深入理解Java内存模型(五)——锁
锁的释放-获取建立的happens before 关系 锁是java并发编程中最重要的同步机制。锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。下面是锁释放-获取的示例代码: class MonitorExample { int a = 0; public synchronized void writer() { //1 a++; //2 }
小小明童鞋
2018/06/13
8840
深度好文 | Java 可重入锁内存可见性分析
周童 来自酒店搜索报价中心,主要负责酒店报价缓存,计算相关系统的开发以及性能优化等工作,热爱摩旅。 一个习以为常的细节 之前在做 ReentrantLock 相关的试验,试验本身很简单,和本文相关的简化版如下:(提示:以下代码均可左右滑动) private static ReentrantLock LOCK = new ReentrantLock(); private static int count = 0; ... // 多线程 run 如下代码 LOCK.lock(); try { count
Java技术栈
2018/06/04
1.1K0
深入理解并发编程艺术-内存模型篇
随着硬件技术的飞速发展,多核处理器已经成为计算设备的标配,这使得开发人员需要掌握并发编程的知识和技巧,以充分发挥多核处理器的潜力。然而并发编程并非易事,它涉及到许多复杂的概念和原理。为了更好地理解并发编程的内在机制,需要深入研究内存模型及其在并发编程中的应用。本文将主要以Java内存模型来探讨并发编程中BUG的源头和处理这些问题的底层实现原理,助你更好地把握并发编程的内在机制。
卡卡罗特杨
2023/06/26
5990
java多线程实现原理
java的内存模式 线程 - 工作内存 - 主存。线程会读写工作内存,CPU会周期性的将工作数据刷入主存,如果多个线程写工作内存,就会导致每个线程的工作内存、主存内存数据都不一致,最终导致执行结果无法预期。
逝兮诚
2019/10/30
8670
2.2 指令重排&happens-before 原则 & 内存屏障
令重排序:java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果 与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的 重排序。
用户7798898
2020/09/27
2K0
2.2 指令重排&happens-before 原则 & 内存屏障
Java并发面试题&知识点总结(下篇)
volatile 是 Java 中用于实现共享变量可见性的关键字。它具有以下特点:
栗筝i
2023/11/15
3160
最详细分析Java 内存模型
并发编程中, 线程之间如何通信及线程之间如何同步, 通信是指线程之间以何种机制来交换 信息。在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递。
Tim在路上
2020/08/04
3380
2024年java面试准备--多线程篇(2)
如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁。其次,进程运行推进顺序与速度不同,也可能会产生死锁。
终有救赎
2023/10/16
2210
2024年java面试准备--多线程篇(2)
3分钟速读原著《Java并发编程的艺术》(一)
总纲介绍: 1.并发编程会遇到的问题以及解决方案 2.Java并发编程的底层实现原理,CPU和JVM是如何帮助解决的 3.Java内存模型,java线程之间的通信 4.多线程技术带来的好处,多线程的生命周期的基本概念 5.Java并发包和锁相关的API和组件,以及这些API和组件的使用方式和实现细节 6.并发容器的实现原理 7.Java中的原子类操作 8.并发工具类 9.线程池的实现原理和使用建议 10.Executor框架和整体结构和成员组件 11.并发编程的实现 第一章 上下文切换:CPU通过实践片
cwl_java
2019/10/26
5510
基础篇:详解锁原理,volatile+cas、synchronized的底层实现
字节码出现了4: monitorenter和14: monitorexit两个指令;字面理解就是监视进入,监视退出。可以理解为代码块执行前的加锁,和退出同步时的解锁
潜行前行
2020/12/11
1.3K0
基础篇:详解锁原理,volatile+cas、synchronized的底层实现
Java内存模型(Java Memory Model,JMM)
多线程、高并发问题相信是每一位从事Java研发工作的程序员都不可回避的一个重要话题。从启动一个线程,到使用volatile、synchronized、final关键字,到使用wait()、notify()、notifyAll()、join()方法,再到编写复杂的多线程程序,不知道大家有没有思考过这样一个问题,为什么要使用这些API,或者说这些API到底给编程人员提供了什么样的保证,才使得在多线程环境下程序的运行结果能够符合预期。它就是Java Memory Model(后续简称JMM)。本文就带领大家一起,绕道这些API的背后,一探究竟。
京东技术
2021/11/25
8700
Java内存模型(Java Memory Model,JMM)
java内存模型的理解
可见性定义: 一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为可见性。
大忽悠爱学习
2023/03/06
3270
java内存模型的理解
JMM和底层实现原理
线程的通信是指线程之间以何种机制来交换信息。在编程中,线程之间的通信机制有两种,共享内存和消息传递。 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信,典型的共享内存通信方式就是通过共享对象进行通信。 在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信,在java中典型的消息传递方式就是wait()和notify()。
allsmallpig
2021/02/25
8980
【小知识大道理】i++是原子运算么
前言: 前端时间写文章经常是想到哪儿写到哪儿,随心所欲,狂放不羁爱自由... 某日灵光一现,何不以一些日常不怎么起眼的知识点作为入口,慢慢延伸,直至把背后的知识体系整个拎起来串联一体,应该能达到不错的学习效果(这个很考验知识厚度,不晓得自己还能拎起来多少 >_<)。 此文作为“小知识大道理”系列的开山篇,现在就ROLL起来。
曲水流觞
2019/11/05
5400
【小知识大道理】i++是原子运算么
java面试线程必备知识点,怼死面试官,从我做起
内存屏障:限制命令操作顺序,有LoadLoad、LoadStore、LoadStore、StroreStreo四种屏障
用户1257393
2018/09/30
4960
面试官:说说volatile应用和实现原理?
volatile 是并发编程中的重要关键字,它的名气甚至是可以与 synchronized、ReentrantLock 等齐名,也是属于并发编程五杰之一。
磊哥
2024/08/14
1420
Java面试:2021.05.17
1、select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
夕梦
2021/05/18
4580
Java面试:2021.05.17
【Java线程】深入理解Volatile关键字和使用
理解volatile底层原理之前,首先介绍关于缓存一致性协议的知识。 背景:计算机在执行程序时,每条指令都是由CPU调度执行的。CPU执行计算指令时,产生与内存(物理内存)通讯的过程(即数据的读取和写入),由于CPU执行速度很快,而从内存读取数据和内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存(Cache)。
沁溪源
2020/12/22
4320
【Java线程】深入理解Volatile关键字和使用
【云+社区年度征文】深入理解Volatile关键字和使用
背景:计算机在执行程序时,每条指令都是由CPU调度执行的。CPU执行计算指令时,产生与内存(物理内存)通讯的过程(即数据的读取和写入),由于CPU执行速度很快,而从内存读取数据和内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存(Cache)。
沁溪源
2020/12/19
3290
【云+社区年度征文】深入理解Volatile关键字和使用
推荐阅读
相关推荐
Volatile实现原理实现原子性happens-before关系从happends-before规则分析可见性编译器层面实现可见性处理器层面实现可见性
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验