ReentrantLock(公平锁、非公平锁)可重入锁原理
2022/7/20 23:23:42
本文主要是介绍ReentrantLock(公平锁、非公平锁)可重入锁原理,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
基本使用
ReentrantLock,位于java.util.concurrent
包,于JDK1.5引入,一种可重入互斥Lock ,其基本行为和语义与使用synchronized方法和语句访问的隐式监视器锁相同,但具有扩展功能。
ReentrantLock的使用也很简单,在源码注释中可以看到使用的推荐方式:
public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock() } }
具体使用代码如下:
package com.starsray.test.lock; import java.util.concurrent.CountDownLatch; import java.util.concurrent.locks.ReentrantLock; public class TestReentrantLock { private final static ReentrantLock lock = new ReentrantLock(); static int threadCount = 5; static int counter = 0; static int threshold = 1000; private static final CountDownLatch latch = new CountDownLatch(threadCount); public static void m() { lock.lock(); try { counter++; } finally { lock.unlock(); } } public static void main(String[] args) throws InterruptedException { for (int i = 0; i < threadCount; i++) { new Thread(() -> { for (int j = 0; j < threshold; j++) { m(); } latch.countDown(); }, "Thread-" + i).start(); } latch.await(); System.out.println("count:" + counter); } }
源码分析
ReentrantLock由上次成功锁定但尚未解锁的线程拥有。当锁不被另一个线程拥有时,调用lock的线程将返回,成功获取锁。如果当前线程已经拥有锁,该方法将立即返回。可以使用方法isHeldByCurrentThread
和getHoldCount
来检查。
ReentrantLock除了支持synchronized
的语义之外,还支持公平锁和非公平锁的拓展,在非公平锁模式下与synchronized
相同,也是其默认的锁模式。构造函数接受一个可选的公平参数,当设置为true时表示为公平锁。
ReentrantLock基于AQS实现,内部维护了一个CLH队列,所谓公平非公平,指的是线程是否按照入队的顺序进行锁的获取。需要注意的是,锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的许多线程之一可能会连续多次获得它,而其他活动线程没有进展并且当前没有持有锁。另请注意,不定时的tryLock()方法不遵守公平设置。如果锁可用,即使其他线程正在等待,它也会成功。
查看ReentrantLock的类图结构,其实现了Lock
接口,其中定义了关于锁的一些基本操作,包含了Sync
、FairSync
、NonfairSync
这几个内部类。从名称也可以看出FairSync
、NonfairSync
分别实现了公平锁和非公平锁,二者同时继承自,作为锁实现的Sync
,并且Sync
继承自AbstractQueuedSynchronizer
。
接下来对ReentrantLock的源码进行分析。
Sync(基础类)
ReentrantLock实现的基础依赖于Sync类,一个静态内部抽象类,继承自AbstractQueuedSynchronizer
,使用AQS中的state状态值来记录持有锁的次数,为下面的公平锁和非公平锁扩展定义好了基础框架。
ReentrantLock中关于加锁根据场景有不同的实现,对应的lock方法分别在Lock
接口和内部类Sync
、FairSync
、NonfairSync
中进行定义和重载,而解锁的方式统一调用的是AQS中定义的变更状态方法,得益于AQS优秀的设计。如下代码所示:
public void unlock() { // 此处release调用为synch子类对象指向Sync继承自AQS的父类方法 sync.release(1); } public void lock() { // 此处lock方法为sync子类对象各自引用的调用 sync.lock(); }
AQS中实现了同步队列阻塞以及释放锁的能力,开放了子类关于锁的添加和释放的方法,tryAcquireShared
、tryAcquire
、tryRelease
、tryReleaseShared
,因此ReentrantLock中也是如此,由于其为互斥锁实现,因此只用重写tryAcquire
、tryRelease
方法的实现即可。这在Sync类中分别对应:
// 默认实现了非公平锁的加锁条件 final boolean nonfairTryAcquire(int acquires) { // 获取当前线程 final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 如果同步状态位为0,当前线程尝试通过CAS去修改状态位 if (compareAndSetState(0, acquires)) { // 如果CAS成功,设置当前线程为独占线程,返回true setExclusiveOwnerThread(current); return true; } } // 如果不为0,判断当前线程是否为独占线程 else if (current == getExclusiveOwnerThread()) { // 如果再次获取锁,记录重入次数 int nextc = c + acquires; if (nextc < 0) // overflow 溢出 throw new Error("Maximum lock count exceeded"); // 更新状态位 setState(nextc); return true; } // 如果线程首次即没有获得锁,也不满足重入条件,返回false,获取锁失败 return false; } // 使用模板方法设计模式,该方法在AQS的release方法中被调用,作为判断是否释放锁状态的前置条件 protected final boolean tryRelease(int releases) { // 每次释放锁对状态为进行递减 int c = getState() - releases; // 如果当前线程不是队列首线程,抛出IllegalMonitorStateException异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); // 当同步状态位state==0时,返回释放锁成功 boolean free = false; if (c == 0) { free = true; // 当前独占线程为null,即没有线程占用 setExclusiveOwnerThread(null); } // 更新同步状态位为初始值 setState(c); return free; }
Sync中tryRelease
对应为父类AQS中的实现,nonfairTryAcquire
对应子类中的公共实现,父类AQS中tryAcquire
的实现分别在FairSync
和NonfairSync
子类中进行实现。
FairSync(公平锁)
由于ReentrantLock基于FIFO的队列来实现线程排队等待,公平锁中的tryAcquire方法,当CPU进行调度时,每次按照队列的排队顺序进行同步状态为值的判断,尝试锁的获取,因此实现了先到先得的公平性。
static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { // 调用AQS的acquire方法,传入参数state值为1 // 是否将当前线程加入到等待队列中,取决于tryAcquire的返回结果 // 如果尝试获取锁或者添加到队列失败,将中断当前线程操作 acquire(1); } // 尝试获取锁的公平锁实现,如果不是队列首线程将获取锁失败 protected final boolean tryAcquire(int acquires) { // 获取当前运行线程 final Thread current = Thread.currentThread(); // 获取AQS的同步状态值 int c = getState(); if (c == 0) { // hasQueuedPredecessors方法的执行结果:如果当前线程之前有排队线程,则为true ,如果当前线程位于队列的头部或队列为空,则为false // 如果同步状态为0,并且当前队列没有线程排队,并且CAS修改状态位成功则尝试获取锁成功 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { // 设置当前线程为独占拥有线程 setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 如果 c != 0,且当前线程为独占线程,记录当前线程重入锁的次数。 int nextc = c + acquires; if (nextc < 0) // 冲入次数溢出了,超过了int表示的最大值 throw new Error("Maximum lock count exceeded"); // 更新AQS同步状态位的值 setState(nextc); return true; } return false; } }
NonfairSync(非公平锁)
关于非公平锁的实现也是相当巧妙,基于队列实现公平锁很简单,如何实现非公平锁呢?如下代码所示:
static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; final void lock() { // 在线程入队前先进性一次CAS尝试,如果成功,则将当前线程设置为独占模式的执行线程 // 其余线程继续保持队列排队等待状态,作者也在注释中说明了锁的公平性并不能保证线程调度的公平性 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else // 如果CAS失败,则进入队列 acquire(1); } protected final boolean tryAcquire(int acquires) { // 调用基础类中的实现 return nonfairTryAcquire(acquires); } }
总结
- ReentrantLock实现了synchronized相同的语义,并且对其进行了扩展,synchronized借助于操作系统层面互斥信号量来实现互斥锁,ReentrantLock借助于AQS抽象同步队列框架来实现锁。
- 公平锁与非公平锁,二者的区别在于,公平锁直接依赖队列的顺序进行线程的执行与挂起,而非公平锁在CPU进行调度时直接进行一次CAS操作,如果成功则获得调度优先级,失败则进入队列等待。
- 在锁的获取方面使用synchronized的锁往往会被一个线程多次获取,而ReentrantLock提供的锁在线程执释放锁后,如果再次获得锁就需要进入队列等待。
参考资料:
- jdk1.8.0_311 ReentrantLock源码
这篇关于ReentrantLock(公平锁、非公平锁)可重入锁原理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-23增量更新怎么做?-icode9专业技术文章分享
- 2024-11-23压缩包加密方案有哪些?-icode9专业技术文章分享
- 2024-11-23用shell怎么写一个开机时自动同步远程仓库的代码?-icode9专业技术文章分享
- 2024-11-23webman可以同步自己的仓库吗?-icode9专业技术文章分享
- 2024-11-23在 Webman 中怎么判断是否有某命令进程正在运行?-icode9专业技术文章分享
- 2024-11-23如何重置new Swiper?-icode9专业技术文章分享
- 2024-11-23oss直传有什么好处?-icode9专业技术文章分享
- 2024-11-23如何将oss直传封装成一个组件在其他页面调用时都可以使用?-icode9专业技术文章分享
- 2024-11-23怎么使用laravel 11在代码里获取路由列表?-icode9专业技术文章分享
- 2024-11-22怎么实现ansible playbook 备份代码中命名包含时间戳功能?-icode9专业技术文章分享