ReentrantLock源码解析——虽众但写

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

ReentrantLock源码解析——虽众但写

张小云的博客   2020-04-01 我要评论
> 在看这篇文章时,笔者默认你已经看过AQS或者已经初步的了解AQS的内部过程。   先简单介绍一下`ReentantLock`,跟`synchronized`相同,是**可重入**的重量级锁。但是其用法则相当不同,首先`ReentrantLock`要**显式的调用lock方法**表示接下来的这段代码已经被当前线程锁住,其他线程需要执行时需要拿到这个锁才能执行,而当前线程在执行完之后要显式的释放锁,固定格式 ``` java lock.lock(); try { doSomething(); } finally { lock.unlock(); } ``` # 1.ReentrantLock的demo程序 来通过下面这段代码简单的了解`ReentrantLock`是如何使用的 ```java // 定义一个锁 private static Lock lock = new ReentrantLock(); /** * ReentrantLock的使用例子,并且验证其一些特性 * @param args 入参 * @throws Exception 错误 */ public static void main(String[] args) throws Exception { // 线程池 ThreadPoolExecutor executor = ThreadPoolUtil.getInstance(); executor.execute(() -> { System.err.println("线程1尝试获取lock锁..."); lock.lock(); try { System.err.println("线程1拿到锁并进入try,准备执行testForLock方法"); // 调用下方的方法,验证lock的可重入性 testForLock(); TimeUnit.MILLISECONDS.sleep(500); System.err.println("线程1try模块全部执行完毕,准备释放lock锁"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); System.err.println("线程1释放lock锁,线程1释放锁2次,此时才算真正释放,验证了ReentrantLock加锁多少次就要释放多少次锁"); } }); // 先睡他100ms,保证线程1先拿到锁 TimeUnit.MILLISECONDS.sleep(100); executor.execute(() -> { System.err.println("线程2尝试获取lock锁..."); lock.lock(); try { System.err.println("线程2拿到锁并进入try"); } finally { lock.unlock(); System.err.println("线程2执行完毕,释放lock锁"); } }); } /** * 验证ReentrantLock具有可重入 */ public static void testForLock() throws InterruptedException { System.err.println("线程1开始执行testForLock方法,正准备获取lock锁..."); lock.lock(); try { System.err.println("testForLock成功获取lock锁,证明了ReentrantLock具有可重入性"); TimeUnit.MILLISECONDS.sleep(200); } finally { lock.unlock(); System.err.println("testForLock释放lock锁,线程1释放锁一次"); } } ``` 结果图:![1585664568146](https://images.cnblogs.com/cnblogs_com/zhangweicheng/1583123/o_2004011435041585664568146.png)   从结果图中,我们得到了很多信息,比如`ReentrantLock`具备**可重入性**(`testForLock`方法得出),并且**其释放锁的次数必须跟加锁的次数保持一致**(这样才能保证正确性);此外`ReentrantLock`为**悲观锁**,在某个线程获取到锁之后其他线程在其完全释放之前不得获取(线程**2**充分证明了这一点,其开始获取锁的时间要比线程**1**的执行时间快许多,但还是被阻塞住了)。 # 2.获取锁的方法——lock()   okay,那来看下其内部是如何实现的,直接点击`lock()`方法 ```java public void lock() { sync.lock(); } ``` 看到其直接调用了`sync`的`lock()`方法,再点击进入 ```java abstract static class Sync extends AbstractQueuedSynchronizer { // ... abstract void lock(); // ... } ```   可以看到`Sync`类是`ReentrantLock`的一个**内部类**,继承了**`AQS`框架**,也就是说`ReentrantLock`就是**AQS框架下的一个产物**,那么问题就变得简单起来了。如果还没了解过`AQS`的可以看下我另一篇文章——[AQS框架详解](https://www.cnblogs.com/zhangweicheng/p/12000213.html),看过之后再回头看`ReentrantLock`,你会发现,**就这?**   扯回来`ReentrantLock`,这边可以看到**内部类**`Sync`是一个抽象类,`lock()`方法也是一个**抽象方法**,也就意味着这个`lock`会根据子类的不同实现执行不同操作,点开子类发现有两个——**公平锁和非公平锁**。 ![1585667693048](https://images.cnblogs.com/cnblogs_com/zhangweicheng/1583123/o_2004011440151585667693048.png) 里边的具体实现先放一放,回到`ReentrantLock`的`lock`方法 ```java public void lock() { sync.lock(); } ```   直接调用说明`sync`已经被初始化过,那么在哪里进行初始化的呢?仔细翻一翻可以从`ReentrantLock`的**两个构造方法**中发现猫腻 ```java /** * 构造方法1 * 无参构造方法,直接将sync初始化为非公平锁 */ public ReentrantLock() { sync = new NonfairSync(); } /** * 构造方法2 * 带参构造方法,根据传进来的布尔值决定将sync初始化为公平还是非公平锁 */ public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); } ```   这里顺带说一下,在`AQS`有一个**同步队列(`CLH`)**,是一种**先进先出队列**。公平锁的意思就是**严格按照这个队列的顺序来获取锁,非公平锁的意思就是不一定按照这个队列的顺序来。**   那现在知道`sync`是在创建`ReentrantLock`的时候就进行了初始化,我们就来看下公平和非公平锁各自做了什么吧。 ## 2.1 非公平锁 ```java static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; final void lock() { // 使用CAS尝试将state改为1,如果成功了,则表示获取锁成功,设置当前线程为持有线程即可 if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else // 否则的话调用AQS的acquire方法乖乖入同步队列等待去吧 acquire(1); } // AQS暴露出来需要子类重写的方法 protected final boolean tryAcquire(int acquires) { // 方法解释在下方 return nonfairTryAcquire(acquires); } } // 非公平锁的tryAcquire方法,该方法是放在Sync抽象类中的,为了tryLock的时候使用 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); // 当前锁的状态 int c = getState(); // 如果是0则表示锁是开放状态,可以争夺 if (c == 0) { // 使用CAS设置为对应的值,在ReentrantLock中acquires的值一直是1 if (compareAndSetState(0, acquires)) { // 成功了设置持有线程 setExclusiveOwnerThread(current); return true; } } /* * 如果当前线程是持有线程,那么state的值+1 * 这里也是ReentrantLock可重入的原理 */ else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } ```   非公平锁基本的流程解释在上方的代码中已经在注释写出,相信不难看懂。不过有个需要注意的点要说一下,首先要看清楚非公平锁的定义,**它是不一定按照队列顺序来获取,不是不按照队列顺序获取。**   从上面的代码我们也可以看出来,非公平锁调用`lock()`方法的时候会先调用一次`CAS`来获取锁,成功了直接返回,**这第一次操作没有按照队列的顺序来,但也只有这一次。**如果**失败了,入队之后还是乖乖的得按照CLH同步队列的顺序来拿锁,**这一点要搞清楚。 ## 2.3 公平锁 ```java static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; // lock方法直接调用AQS的acquire方法,连一点争取的欲望都没有 final void lock() { acquire(1); } // 公平锁的获取资源方法,该方法是在acquire方法类调用的 protected final boolean tryAcquire(int acquires) { // 整体逻辑还是挺简单的,跟非公平有些类似 final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { /* * c==0表示当前锁没有被获取 * 如果没有前驱节点或者前驱节点是头结点, * 那么使用CAS尝试获取资源 * 成功了设置持有线程并返回true,失败了直接返回 */ 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; } } ```   公平锁的逻辑相对来说十分简单,`lock`方法老老实实的去排队获取锁,而获取资源方法的逻辑也在代码注释写得很清楚了,没有什么需要多讲的。 # 3.锁释放 上面的理解之后释放锁的逻辑就简单的多了,直接放代码吧: ```java /* * 解锁方法直接调用AQS的release方法 * 而release方法的去向又是跟tryRelease的返回值直接相关 * tryRelease方法的实现在内部类Sync中,具体在下方 */ public void unlock() { sync.release(1); } abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L; // ... // 释放资源的方法 protected final boolean tryRelease(int releases) { // 拿到当前锁的加锁次数 int c = getState() - releases; // 当前线程必须是锁持有线程才能操作 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; // 如果次数为0,表示完全释放,清空持有线程 if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } // ... } ```   释放锁的逻辑在注释中解释得很清楚了,看完也知道由于`ReentrantLock`是可重入的,所以锁的数值会逐渐增加,那么在**释放的时候也要一个一个逐一释放**。 主要的逻辑还是`AQS`的`release`方法中,这里详讲的话篇幅太多,有兴趣的话可以单独看下`AQS`的文章,传送门:[AQS](https://www.cnblogs.com/zhangweicheng/p/12000213.html)。 # 4.ReentrantLock的可选择性   来讲下`ReentrantLock`跟`Synchonized`的一大不同点之一——`Condition`。那么`condition`是什么呢,简单来说就是将**等待获取资源的线程独立出来分队**,什么意思呢?举个例子,现在有8个线程同时争取一个锁,我觉得太多了,就把这个8个线程平均分成4队,等我觉得哪队OK就将那一队的线程叫出来争取这个锁。在这里的`condition`就是队伍,4队就是4个`condition`。   另外说一句,**`condition`(队伍)中的线程是不参与锁的竞争**的,如果上方的8个线程我只将2个线程放入一个队,其他线程不建立队伍,那么**其他线程会参与锁的竞争,而独立到队伍中的2个线程则不会**,因为其被放在`AQS`的**等待队列**中,**等待队列是不参与资源的竞争**的,我在另一篇文章——[AQS框架详解](https://www.cnblogs.com/zhangweicheng/p/12000213.html)写得很清楚了。还是那句话,`AQS`懂了再看`ReentrantLock`,理解难度就会低得多得多得多得多.... okay,那来简单看下`Condition`如何使用 ```java // 线程池 ThreadPoolExecutor executor = ThreadPoolUtil.getInstance(); // 这里只建了一个condition起理解作用,自己有兴趣的话可以多建几个模拟多点场景 Condition condition = lock.newCondition(); executor.execute(() -> { System.err.println("线程1尝试获取lock锁..."); lock.lock(); try { System.err.println("线程1拿到锁并进入try"); System.err.println("线程1准备进行condition操作"); /* * 将当前线程即线程1放入指定的这个condition中, * 如果是其他condition则调用其他condition的await()方法 */ condition.await(); System.err.println("线程1结束condition操作"); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); System.err.println("线程1执行完毕,释放lock锁"); } }); // 保证线程1获取锁并且执行完毕 TimeUnit.MILLISECONDS.sleep(200); executor.execute(() -> { System.err.println("线程2尝试获取lock锁..."); lock.lock(); try { System.err.println("线程2拿到锁并进入try"); // 唤醒condition的所有线程 condition.signalAll(); System.err.println("线程2将condition中的线程唤醒"); } finally { lock.unlock(); System.err.println("线程2执行完毕,释放lock锁"); } }); ``` 结果图: ![1585749193417](https://images.cnblogs.com/cnblogs_com/zhangweicheng/1583123/o_2004011451411585749193417.png) 可以从结果图中看到,   当线程调用了`condition.await()`的时候就被放入了`condition`中,并且此时**将持有的锁释放,将自己挂起睡觉等待其他线程唤醒。**所以线程2才能在线程1没执行完的情况获取到了锁,并且线程2执行完操作之后将线程1唤醒,线程1此时其实是**重新进入同步队列(队尾)争取资源**的,如果队列前方还有线程在等待的话它是不会拿到的,要按照队列顺序获取,可以自己在本地创多几个线程试一下。   通过这段简单的代码之后明显可以看到`condition`具有不错的灵活性,也就是说提供了更多了**选择性**,这也就是跟`synchronized`不同的地方,如果使用`synchronized`加锁,那么`Object`的唤醒方法只能唤醒全部,或者其中的一个,但是`ReentrantLock`不同,有了`condition`的帮助,可以不同的线程进行不同的分组,然后有选择的**唤醒其中的一组**或者**其中一组的随机一个。** # 5.总结   `ReentrantLock`的源码如果有了`AQS`的基础,那么看起来是不费吹灰之力(开个玩笑,还是要比吹灰费劲的)。所以本章的篇幅也比较简单,先从一个例子说明了`ReentrantLock`的用法, 并且通过这个例子介绍了`ReentrantLock`**可重入、悲观锁**的几个特性;接着对其`lock`方法进行源码跟踪,从而了解到其内部的方法都是由继承`AQS`的内部类`Sync`来实现的,而`Sync`又分成了两个类,**代表两种不同的锁**——**公平锁和非公平锁**;接下来再讲到两种锁的具体实现和释放的逻辑,到这里加锁解锁的流程就完整了;最后再介绍`ReentrantLock`的另一种特性——`Condition`,这种特性允许其选择特定的线程来争夺锁,也可以选择性的唤醒锁,到这里整篇文章就告一段落。       > 孤独的人不一定是天才,还可能是得了郁抑症。

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们