ReentrantLock源码解析

简介: ReentrantLock源码解析

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

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

相对于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的目的就是为了记录当前锁被同一个线程获取了几次。但是看完这段代码你肯定没看出来哪__不公平了__。别急,我们来对比下公平锁的实现就知道了。   
 ```java
   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保证线程安全的,所以后面的逻辑也一定是线程安全的。

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

目录
相关文章
|
4天前
|
云安全 人工智能 算法
以“AI对抗AI”,阿里云验证码进入2.0时代
三层立体防护,用大模型打赢人机攻防战
1315 4
|
4天前
|
机器学习/深度学习 安全 API
MAI-UI 开源:通用 GUI 智能体基座登顶 SOTA!
MAI-UI是通义实验室推出的全尺寸GUI智能体基座模型,原生集成用户交互、MCP工具调用与端云协同能力。支持跨App操作、模糊语义理解与主动提问澄清,通过大规模在线强化学习实现复杂任务自动化,在出行、办公等高频场景中表现卓越,已登顶ScreenSpot-Pro、MobileWorld等多项SOTA评测。
660 3
|
5天前
|
人工智能 Rust 运维
这个神器让你白嫖ClaudeOpus 4.5,Gemini 3!还能接Claude Code等任意平台
加我进AI讨论学习群,公众号右下角“联系方式”文末有老金的 开源知识库地址·全免费
|
11天前
|
编解码 人工智能 自然语言处理
⚽阿里云百炼通义万相 2.6 视频生成玩法手册
通义万相Wan 2.6是全球首个支持角色扮演的AI视频生成模型,可基于参考视频形象与音色生成多角色合拍、多镜头叙事的15秒长视频,实现声画同步、智能分镜,适用于影视创作、营销展示等场景。
766 6
|
8天前
|
物联网 API UED
Qwen-Image-Edit-2511来啦!角色一致性再提升,LoRA能力内置
Qwen-Image-Edit-2511发布!提升角色与多人合照一致性,集成Lora打光、新视角生成,增强工业设计与几何推理能力。已开源,支持魔搭、QwenChat免费体验,本地部署可获最佳效果。
465 3