ReentrantLock源码解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 谈到多线程,就不避开锁(Lock),jdk中已经为我们提供了好几种锁的实现,已经足以满足我们大部分的需求了,今天我们就来看下最常用的ReentrantLock的实现。

谈到多线程,就不避开锁(Lock),jdk中已经为我们提供了好几种锁的实现,已经足以满足我们大部分的需求了,今天我们就来看下最常用的ReentrantLock的实现。


其实最开始是想写一篇关于StampedLock的源码分析的,但发现写StampedLock前避不开ReentrantReadWriteLock,写ReentrantReadWriteLock又避不开ReentrantLock,他们仨是逐层递进的关系。ReentrantReadWriteLock解决了一些ReentrantLock无法解决的问题,StampedLock又弥补了ReentrantReadWriteLock的一些不足,三者有各自的设计和有缺点,这篇文章先和你一起看下ReentrantLock,之后我们会再一起去了解ReentrantReadWriteLock和StampedLock,相信有了ReentrantLock的基础后面的内容也会容易理解很多。

3ef124ae6652eaed38a6410fd5534c01_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly94aW5kb28uYmxvZy5jc2RuLm5ldA==,size_16,color_FFFFFF,t_70.png

相对于jdk中很多其他的类来说,ReentrantLock提供的接口已经算是非常简单,事实上它只有一个构造参数boolean fair,用来指定是公平锁还是非公平锁,如果你指定的话默认是非公平锁。

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }

什么是公平?这里的的公平是指每个线程可以有公平的机会获取到这把锁,10个线程竞争这把锁,某个线程各有10%的机会获取到锁。听起来很理想主义,但大多数时候不建议使用公平锁,因为局部性的存在,每个线程对锁的真正需求度是不同的,有些线程就是需要很频繁的占有锁,有些偶尔占有就行。如果你单纯是为了公平而导致供需不平衡,可能有些线程会浪费锁的持有时间,而有些线程急需用锁但迟迟获取不到,导致线程饥饿,最终导致整个系统的性能不是最大化的。


最大化锁的使用率和代码性能就成了锁设计最重要的目标。试想如果我们提前知道每个线程对锁的需求度,然后按需求度给他们分配锁的占有机会,这样必然能达到锁的最优使用率。但实际上对于jdk的开发者来说,他哪知道你要拿锁去做啥、要开几个线程?所以ReentrantReadWriteLock的设计者用一种很简单粗暴的方式解决了大部分的问题,我们直接上源码。


ReentrantLock中最核心的就是Sync的实现,它默认已经实现了非公平锁的功能,所以你会看到NonfairSync只是简简单单继承了Sync而已。而Sync的主要功能还是继承了AbstractQueuedSynchronizer(AQS)。


AbstractQueuedSynchronizer 简单来说就是维护了一个状态 state,和一个等待线程队列(一个双向链表),然后通过VarHandle提供的CAS操作保证线程安全。当你在调用lock()的时候,会直接调用到AbstractQueuedSynchronizer中的

acquire(int arg)
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

acquire也很简单,尝试去获取锁,如果获取失败,就把当前线程加到等待队列里(排队),并把当前线程的状态设置为可中断。


回到ReentrantLock,我们来开下它是如何依赖Sync来实现非公平锁的。NonfairSync在执行tryAcquire(int arg)的时候,实际执行的是以下代码。


final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {   //如果state是0,说明当前没有线程持有锁,用CAS更新状态,如果CAS成功,就在锁中写入当前线程的信息。  
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {  //如果state不是0,也不一定获取锁失败,要看下持有锁的线程是不是自己,如果是更新state 
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }


从这可以看出来ReentrantLock是可重入锁,state的目的就是为了记录当前锁被同一个线程获取了几次。但是看完这段代码你肯定没看出来哪 不公平了。别急,我们来对比下公平锁的实现就知道了。

static final class FairSync extends Sync {
       private static final long serialVersionUID = -3000897897090466540L;
       @ReservedStackAccess
       protected final boolean tryAcquire(int acquires) {
           final Thread current = Thread.currentThread();
           int c = getState();
           if (c == 0) {
               if (!hasQueuedPredecessors() &&    // 不同之处
                   compareAndSetState(0, acquires)) {
                   setExclusiveOwnerThread(current);
                   return true;
               }
           }
           else if (current == getExclusiveOwnerThread()) {
               int nextc = c + acquires;
               if (nextc < 0)
                   throw new Error("Maximum lock count exceeded");
               setState(nextc);
               return true;
           }
           return false;
       }
   }


FairSync也是继承自Sync,但它重写了加锁tryAcquire方法,打眼一看和上面非公平锁的tryAcquire非常像,唯一不同之处就是在state为0时多了个!hasQueuedPredecessors()的判断。hasQueuedPredecessors()方法是判断是否有线程在等待去获取这把锁,如果有其他线程这次就算是获取锁失败了。


来个易懂的例子,现在办公室只有一间卫生间,很多个同事共用,有人在用卫生间的时候也不会希望别人跑进来和他一起用。公平锁的实现方式就是我来上卫生间,发现卫生间没人用,但有人在排队等卫生间(可能是玩手机没注意卫生间空了),我只能乖乖排队。非公平锁的实现方式是,我来上卫生间,发现卫生间是空的,不管有没有人排队我都占了,这样显然对其他排队的人来说是不公平的。


这种方式在现实世界看起来是非常不合理的,但是如果换种视角,可能越着急的人才是越需要用卫生间的人(可能他拉肚子),让排队的人多等会无所谓,这样才能最大化卫生间的的价值。虽然拉肚子在现实世界不常见,在在计算机中以纳秒计的世界里,有些线程就是比其他线程急很多的情况非常常见,非公平的方式就很合情。再从概率的角度看,如果有个线程需要以更高的频次使用这把锁,不排队去获取锁能舍得锁被获取到的次数最大化,也很合理。所以非公平锁合情合理。但历史告诉我们,凡事没有绝对,还是需要具体问题具体分析,有些情况下,非公平锁会导致线程饥饿。


 

public void unlock() {
        sync.release(1);
    }


public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }


unlock()调用了sync中的release(),release()是继承自AQS,跳到AQS中就会发现又调用了tryRelease()。ReentrantLock重写了tryRelease(),源码如下,也比较简单。

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }


释放锁的过程是先判断是否是锁持有线程,然后更新锁状态。如果你进到setExclusiveOwnerThread(null)和setState(c)里面,就会发现这里没有用到CAS,会不会出现线程安全的问题?仔细想想其实不会有线程安全的问题,if (Thread.currentThread() != getExclusiveOwnerThread())判断了是当前线程是否持有锁,保证后续逻辑只有持有锁的线程才会执行到,因为之前获取锁是用CAS保证线程安全的,所以后面的逻辑也一定是线程安全的。

4c9f99b129ecddc7d4892b6b5b5e5dd1_20191220191617520.png

除了加锁和释放锁外,ReentrantLock还提供了和锁、线程相关的的接口,如上图,从函数名就可以看出其作用了,而且实现代码比较简单,这里就不再赘述了,有兴趣可以自行查看源码。

目录
相关文章
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
77 2
|
3天前
|
存储 设计模式 算法
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。 行为型模式分为: • 模板方法模式 • 策略模式 • 命令模式 • 职责链模式 • 状态模式 • 观察者模式 • 中介者模式 • 迭代器模式 • 访问者模式 • 备忘录模式 • 解释器模式
【23种设计模式·全精解析 | 行为型模式篇】11种行为型模式的结构概述、案例实现、优缺点、扩展对比、使用场景、源码解析
|
3天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。 结构型模式分为以下 7 种: • 代理模式 • 适配器模式 • 装饰者模式 • 桥接模式 • 外观模式 • 组合模式 • 享元模式
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
3天前
|
设计模式 存储 安全
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是"将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。创建型模式分为5种:单例模式、工厂方法模式抽象工厂式、原型模式、建造者模式。
【23种设计模式·全精解析 | 创建型模式篇】5种创建型模式的结构概述、实现、优缺点、扩展、使用场景、源码解析
|
26天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
54 12
|
22天前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
3天前
|
安全 搜索推荐 数据挖掘
陪玩系统源码开发流程解析,成品陪玩系统源码的优点
我们自主开发的多客陪玩系统源码,整合了市面上主流陪玩APP功能,支持二次开发。该系统适用于线上游戏陪玩、语音视频聊天、心理咨询等场景,提供用户注册管理、陪玩者资料库、预约匹配、实时通讯、支付结算、安全隐私保护、客户服务及数据分析等功能,打造综合性社交平台。随着互联网技术发展,陪玩系统正成为游戏爱好者的新宠,改变游戏体验并带来新的商业模式。
|
2月前
|
缓存 Java 程序员
Map - LinkedHashSet&Map源码解析
Map - LinkedHashSet&Map源码解析
81 0
|
2月前
|
算法 Java 容器
Map - HashSet & HashMap 源码解析
Map - HashSet & HashMap 源码解析
65 0
|
2月前
|
存储 Java C++
Collection-PriorityQueue源码解析
Collection-PriorityQueue源码解析
69 0

热门文章

最新文章

推荐镜像

更多