seo网站推广首页排名,找人做软件网站,国内大的网站建设公司排名,多城市网站设计图解java.util.concurrent并发包源码系列——深入理解ReentrantLock#xff0c;看完可以吊打面试官 ReentrantLock是什么#xff0c;有什么作用ReentrantLock的使用ReentrantLock源码解析ReentrantLock#lock方法FairSync#tryAcquire方法NonfairSync#tryAcquire方法 Reentrant… 图解java.util.concurrent并发包源码系列——深入理解ReentrantLock看完可以吊打面试官 ReentrantLock是什么有什么作用ReentrantLock的使用ReentrantLock源码解析ReentrantLock#lock方法FairSync#tryAcquire方法NonfairSync#tryAcquire方法 ReentrantLock#unlock方法 ReentrantLock的其他方法简介ReentrantLock#lockInterruptiblyReentrantLock#tryLockReentrantLock#tryLock带参数ReentrantLock#newCondition 往期文章
人人都能看懂的图解java.util.concurrent并发包源码系列 ThreadPoolExecutor线程池图解java.util.concurrent并发包源码系列原子类、CAS、AtomicLong、AtomicStampedReference一套带走图解java.util.concurrent并发包源码系列——LongAdder图解java.util.concurrent并发包源码系列——深入理解AQS看完可以吊打面试官
上一篇文章图解java.util.concurrent并发包源码系列——深入理解AQS看完可以吊打面试官 介绍了AQS的原理和源码然后使用AQS实现了一个排他锁这次我们看看Java的并发包里面基于AQS实现的锁工具类——ReentrantLock。本文会介绍ReentrantLock的功能以及ReentrantLock如何通过AQS实现它的功能但是不会再介绍AQS的相关原理关于AQS的原理和源码可以看上一篇文章。
ReentrantLock是什么有什么作用
ReentrantLock是可重入锁当我们有一些资源需要互斥访问或者有某一块代码需要互斥执行时可以使用ReentrantLock加锁对互斥资源和互斥代码块起到一个保护作用保证同一时刻只会有一个线程访问。 可重入的意思就是当一个线程获取到锁之后如果它再次获取同一把锁是可以获取成功的。而不可重入锁则是当前线程获取到锁以后如果它再次获取是获取不成功的也就是自己把自己给阻塞住了我们上一篇文章最后面的例子就是一个不可重入锁。 ReentrantLock的使用
ReentrantLock的用法非常简单它提供了lock方法用于获取锁unlock方法用于释放锁。
调用lock方法后当前线程会去获取锁获取不到锁会阻塞等待直到获取到锁该方法才会返回。如果重复调用多次lock方法就重复加锁多次当前线程不会阻塞可重入但是它需要多次释放锁。
调用unlock方法当前线程就会释放锁然后唤醒其他阻塞等待获取锁的线程。
我们还是用上一篇文章最后的那个例子去观察ReentrantLock的作用。 public static void main(String[] args) throws InterruptedException {int[] num {0};// 创建一个ReentrantLock对象ReentrantLock reentrantLock new ReentrantLock();Thread[] threads new Thread[100];for (int i 0; i 100; i) {Thread thread new Thread(() - {for (int j 0; j 10000; j) {try {// 加锁reentrantLock.lock();num[0];} finally {// 释放锁防止finally块中是因为防止抛异常导致锁得不到释放reentrantLock.unlock();}}});thread.start();threads[i] thread;}for (int i 0; i threads.length; i) {threads[i].join();}System.out.println(num[0]);}可以看到输出结果是正确的。
用法跟我们上一篇文章最后面的例子几乎是一模一样的。首先要创建一个ReentrantLock对象然在线程try代码块中调用ReentrantLock的lock方法进行加锁在finally代码块中调用ReentrantLock的unlock方法进行解锁操作解锁操作在finally块中是因为防止线程抛异常导致锁得不到释放。
ReentrantLock源码解析
接下来看看ReentrantLock的lock方法和unlock方法的内部源码看看它是怎么实现锁的获取和释放的。
ReentrantLock#lock方法
ReentrantLock#lock public void lock() {sync.lock();}可以看到和我们上一篇文章的例子一样都是有一个内部类这个内部类肯定继承了AQS。但是它这里没有直接调用AQS的acquire方法而是调用了一个lock方法我们进去lock方法看一看。
abstract static class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID -5179523762034025860L;abstract void lock();// ...省略了下面的代码
}可以看到Sync继承了AQS但是Sync本身还是一个抽象类而lock方法也是一个抽象方法。 可以看到Sync有两个实现类FairSync和NonfairSync。 也就是说ReentrantLock内部的sync有可能是FairSync也有可能是NonfairSync。那什么时候是FairSync、什么时候是NonFairSync呢我们看看ReentrantLock的构造方法就知道了。 /*** 默认非公平锁*/public ReentrantLock() {sync new NonfairSync();}/*** fair为true时是公平锁为false则是非公平锁*/public ReentrantLock(boolean fair) {sync fair ? new FairSync() : new NonfairSync();}ReentrantLock可以实现公平锁和非公平锁的功能默认是非公平锁如果我们调用有参构造方法传递的fair参数是true就是公平锁使用的sync就是FairSync否则就是非公平锁使用的就是NonfairSync。 公平锁就是当一个线程获取锁时会先看一下队列中是否有线程等待获取锁如果有那么它会入队列。非公平锁则相反如果一个线程要获取锁那么不管三七二十一先尝试获取一下如果获取成功则不用进队列。非公平锁的效率是比公平锁高的所有默认的就是非公平锁。
我看一下lock方法的具体实现。 static final class FairSync extends Sync {private static final long serialVersionUID -3000897897090466540L;final void lock() {acquire(1);}// ......省略下面的代码}FairSync的lock方法直接调用了AQS的acquire方法与我们上一篇文章的例子一致。 static final class NonfairSync extends Sync {private static final long serialVersionUID 7316153563782823691L;final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}// ......省略下面的代码}NonfairSync的lock方法会使用AQS的compareAndSetState方法尝试获取锁如果CAS成功那么就不需要走acquire方法的逻辑了如果CAS失败才会调用AQS的acquire方法。 FairSync#tryAcquire方法
FairSync的lock方法会调用AQS的acquire方法acquire方法会调用tryAcquire方法然后会进入到FairSync的tryAcquire方法。 protected final boolean tryAcquire(int acquires) {final Thread current Thread.currentThread();int c getState(); // 获取AQS中的state变量if (c 0) { // state等于0表示锁没有被获取if (!hasQueuedPredecessors() // 查看队列中是否有等待获取锁的线程compareAndSetState(0, acquires)) { // CAS尝试获取锁setExclusiveOwnerThread(current); // 获取锁成功设置当前线程为占有锁的线程return true; // 返回true表示告诉AQS获取锁成功}}else if (current getExclusiveOwnerThread()) { // 锁已被获取但是获取锁的线程是当前线程int nextc c acquires; // state加acquiresif (nextc 0)throw new Error(Maximum lock count exceeded);setState(nextc); // 设置state的值return true; // 返回true表示告诉AQS获取锁成功}return false; // 返回true表示告诉AQS获取锁不成功}}FairSync的tryAcquire方法首先会调用AQS的getState方法获取state变量这里是当state为0才表示锁没有被获取与我们上一篇文章的例子不一样。
如果state为0那么锁没有被获取此时会调用hasQueuedPredecessors()方法检查队列中是否有等待获取锁的线程如果有那么当前线程不会获取锁而是返回false直接进队列排队如果队列中没有等待获取锁的线程那么会调用AQS的compareAndSetState方法尝试修改state变量修改成功表示获取锁成功此时就会设置当前线程为占有锁的线程然后方法返回true表示获取锁成功。
如果state不为0那么会检查当前线程是否是占有锁的线程如果是那么会执行重入的逻辑然后方法返回true。如果当前线程不是占有锁的线程那么方法返回false表示获取锁失败。 下面看一下hasQueuedPredecessors方法是怎么判断队列中是否有线程的。 public final boolean hasQueuedPredecessors() {Node t tail; // 尾节点Node h head; // 头节点Node s;return h ! t ((s h.next) null || s.thread ! Thread.currentThread());}h ! t 判断头节点和尾节点是否不相等不相等才有可能有线程在队列中相等的话表示队列为空那么肯定是没有线程在队列中的。
如果头节点和尾节点不相等了那么看h.next头节点的后继节点是否为空如果为空那么就表示当前队列已经有等待获取锁的线程。那么为什么头尾节点不相等并且头节点的next指针为空就表示有线程已经入队列了呢还有什么情况下头节点指针和尾节点指针不相等但是头节点的next指针为null呢答案就在AQS初始化队列以及线程节点入队的方法中也就是AQS的enq方法。 private Node enq(final Node node) {for (;;) {Node t tail;if (t null) { // Must initializeif (compareAndSetHead(new Node())) // A使用CAS的方式设置头节点指针指向一个空节点tail head; // B设置尾指针指向和头指针指向的同一节点} else {node.prev t; // C设置node的prev指针指向当前队列的尾节点if (compareAndSetTail(t, node)) { // D使用CAS的方式设置尾节点指针指向当前nodet.next node; // E设置原来的尾节点的next指针指向当前nodereturn t;}}}}可以看到AQS的enq方法分成了5个步骤。注意这里的前提是AQS的队列还未初始化
假设现在AQS队列还未初始化在当前线程获取锁之前有一个线程执行到了enq方法然后这个线程执行完A、B、C、D四步也就是它成功初始化了AQS的队列然后又用CAS的方式修改尾节点的指针指向它自己的node节点但是就是没有执行代码E此时是不是头节点指针和尾节点指针不相等h ! t并且头节点的next指针是null(s h.next) null此时这个线程已经算是入了队列的如果此时有其他线程来获取锁公平锁那么是应该要排队的。所以当前线程获取锁的时候执行到hasQueuedPredecessors()方法就会返回true表示已经有线程在队列中等待获取锁了。
那么假设现在AQS队列还未初始化如果在当前线程获取锁之前有一个线程执行到了enq方法然这个线程执行完A、B没有执行后面三步呢那么此时头节点指针和尾节点指针还是相等的h ! t 不成立此时这个线程也不算入队列所以此时如果有别的线程来调用hasQueuedPredecessors()方法hasQueuedPredecessors()方法放回false队列中没有等待获取锁的线程也是正确的。
如果头节点指针和尾节点指针不相等h ! t 为true然后头节点的next指针不为null(s h.next) null 为false但是头节点的next指针指向的节点对应的线程等于当前线程s.thread ! Thread.currentThread() 为false那么hasQueuedPredecessors()方法返回false当前线程可以尝试获取锁也是正确的因为很明显这种情况就是我已经在排队了并且此时轮到我了。如果头节点的next指针指向的节点对应的线程不是当前线程s.thread ! Thread.currentThread() 为truehasQueuedPredecessors()方法返回true那么当前线程就只能去排队。 FairSync的tryAcquire方法就介绍到这里。 NonfairSync#tryAcquire方法
NonfairSync的tryAcquire方法里面直接调用了nonfairTryAcquire方法。 protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}nonfairTryAcquire方法位于Sync抽象类内部。 final boolean nonfairTryAcquire(int acquires) {final Thread current Thread.currentThread();int c getState();if (c 0) {// 锁没有被获取尝试获取锁if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// 锁已被获取查看是否是当前线程获取的如果是那么执行重入逻辑else if (current getExclusiveOwnerThread()) {int nextc c acquires;if (nextc 0) // overflowthrow new Error(Maximum lock count exceeded);setState(nextc);return true;}// 获取锁失败return false;}进入到Sync的nonfairTryAcquire方法内部如果判断锁没有被获取state 0就直接先CAS一下如果CAS成功该方法就返回true表示获取锁成功。如果锁已被获取那么判断当前线程是否是持有锁的线程 current getExclusiveOwnerThread()如果当前线程是持有锁的线程那么执行锁重入的逻辑然后方法返回true表示获取锁成功。否则返回false表示获取锁失败。 ReentrantLock的lock方法到这里就介绍完了。 ReentrantLock#unlock方法
接下来看一下ReentrantLock的unlock方法的具体逻辑。 public void unlock() {sync.release(1);}ReentrantLock的unlock方法调用了sync的release方法这个和我们上一篇文章的例子是一样的。
AQS的release方法会调用tryRelease方法。因为释放锁的操作不需要区分公平还是非公平都是一样的释放锁的逻辑所以tryRelease方法就放到了Sync抽象类的内部。也就是说FairSync和NonfairSync的tryRelease是同一个方法都是Sync的tryRelease方法。 protected final boolean tryRelease(int releases) {int c getState() - releases;// 如果当前线程不是持有锁的线程抛异常if (Thread.currentThread() ! getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free false;// c 0表示锁被释放free设为true表示锁被释放设置持有锁的线程为nullif (c 0) {free true;setExclusiveOwnerThread(null);}// 更新statesetState(c);return free;}Sync的tryRelease方法首先会调用AQS的getState方法获取state变量然后计算state变量减去参数releases之后的值c。然后判断如果c等于0表示锁被释放了那么设置返回值free为true表示锁被释放然后setExclusiveOwnerThread(null)设置持有锁的线程为null。然后不管c是否等于0最后都会调用AQS的setState方法更新state变量最后返回free。
如果返回的free为true表示锁被释放了如果返回的free为false表示锁还没有被释放。比如当前线程重复获取锁5次现在调用了unlock方法3次那么state等于2锁还没有被释放它需要再调用两次state才等于0此时锁才被真正释放。 到这里ReentrantLock的unlock方法也介绍完了。
ReentrantLock的其他方法简介
ReentrantLock不仅仅只有lock和unlock两个方法还有其他获取和释放锁的方法下面做一个简单介绍不做深入的描述。
ReentrantLock#lockInterruptibly public void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);}ReentrantLock的lockInterruptibly是以响应中断的方式获取锁就是说在获取锁的过程中如果其他线程调用了thread.interrupt()方法打断了当前线程那么当前线程会响应中断不再继续获取锁。而我们上面介绍的ReentrantLock#lock方法是不响应中断的也就是说其他线程调用了当前线程的thread.interrupt()方法当前线程也不做任何响应。
ReentrantLock#tryLock public boolean tryLock() {return sync.nonfairTryAcquire(1);}ReentrantLock的tryLock方法是尝试获取锁也就是尝试获取一下成就成不成就走也不会阻塞。
ReentrantLock#tryLock带参数 public boolean tryLock(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(timeout));}ReentrantLock还有一个带参数的tryLock方法 boolean tryLock(long timeout, TimeUnit unit)该方法和无参的tryLock方法一样也是尝试获取锁成就成不成不会马上走而是阻塞等待一段时间。timeout参数就是指定阻塞等待的时间unit则是时间单位。
ReentrantLock#newCondition public Condition newCondition() {return sync.newCondition();}ReentrantLock的newCondition可以返回一个Condition对象Condition 可以实现类似于和synchronized加Object#wait的功能这个我们后面的文章再介绍吧。