java多线程(6)—ReentrantLock

越努力越幸运!

彻底理解ReentrantLock

 转载自:https://www.javazhiyin.com/6265.html

1. ReentrantLock的介绍

ReentrantLock重入锁,实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。

    在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。

与此同时,ReentrantLock还支持公平锁和非公平锁两种方式

那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:重入性的实现原理; 公平锁和非公平锁

2. 重入性的实现原理

要想支持重入性,就要解决两个问题:

在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功; 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。

,我们知道,同步组件主要是通过重写AQS的几个protected方法来表达自己的同步语义。针对第一个问题,我们来看看ReentrantLock是怎样实现的,以非公平锁为例,判断当前线程能否获得锁为例,核心方法为nonfairTryAcquire:

final boolean nonfairTryAcquire(int acquires) {
   final Thread current = Thread.currentThread();
   int c = getState();
   //1. 如果该锁未被任何线程占有,该锁能被当前线程获取
 if (c == 0) {
       if (compareAndSetState(0, acquires)) {
           setExclusiveOwnerThread(current);
           return true;
       }
   }
 //2.若被占有,检查占有线程是否是当前线程
   else if (current == getExclusiveOwnerThread()) {
   // 3. 再次获取,计数加一
       int nextc = c + acquires;
       if (nextc < 0) // overflow
           throw new Error("Maximum lock count exceeded");
       setState(nextc);
       return true;
   }
   return false;
}

        这段代码的逻辑也很简单,具体请看注释。为了支持重入性,在第二步增加了处理逻辑,如果该锁已经被线程所占有了,会继续检查占有线程是否为当前线程,如果是的话,同步状态加1返回true,表示可以再次获取成功。

          每次重新获取都会对同步状态进行加一的操作,那么释放的时候处理思路是怎样的了?(依然还是以非公平锁为例)核心方法为tryRelease:

protected final boolean tryRelease(int releases) {
 //1. 同步状态减1
   int c = getState() - releases;
   if (Thread.currentThread() != getExclusiveOwnerThread())
       throw new IllegalMonitorStateException();
   boolean free = false;
   if (c == 0) {
   //2. 只有当同步状态为0时,锁成功被释放,返回true
       free = true;
       setExclusiveOwnerThread(null);
   }
 // 3. 锁未被完全释放,返回false
   setState(c);
   return free;
}

         代码的逻辑请看注释,需要注意的是,重入锁的释放必须得等到同步状态为0时锁才算成功释放,否则锁仍未释放。如果锁被获取n次,释放了n-1次,该锁未完全释放返回false,只有被释放n次才算成功释放,返回true。

          到现在我们可以理清ReentrantLock重入性的实现了,也就是理解了同步语义的第一条。

3. 公平锁与公平锁

ReentrantLock支持两种锁:公平锁和非公平锁。何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。ReentrantLock的构造方法无参时是构造非公平锁,源码为:

public ReentrantLock() {
   sync = new NonfairSync();
}

另外还提供了另外一种方式,可传入一个boolean值,true时为公平锁,false时为非公平锁,源码为:

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

     在上面非公平锁获取时(nonfairTryAcquire方法)只是简单的获取了一下当前状态做了一些逻辑处理,并没有考虑到当前同步队列中线程等待的情况。我们来看看公平锁的处理逻辑是怎样的,核心方法为:

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;
 }
}

            这段代码的逻辑与nonfairTryAcquire基本上一直,唯一的不同在于增加了hasQueuedPredecessors的逻辑判断,方法名就可知道该方法用来判断当前节点在同步队列中是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。

          如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。

公平锁  VS  非公平锁

公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。

公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。

详解Condition的await和signal等待/通知机制

Condition简介

任何一个java对象都天然继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如wait(),wait(long timeout),wait(long timeout, int nanos)与notify(),notifyAll()几个方法实现等待/通知机制,同样的, 在java Lock体系下依然会有同样的方法实现等待/通知机制。

从整体上来看Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性。两者除了在使用方式上不同外,在功能特性上还是有很多的不同:

  • Condition能够支持不响应中断,而通过使用Object方式不支持;

  • Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个;

  • Condition能够支持超时时间的设置,而Object不支持

参照Object的wait和notify/notifyAll方法,Condition也提供了同样的方法:

针对Object的wait方法

  1. void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常;

  2. long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者超时;

  3. boolean await(long time, TimeUnit unit)throws InterruptedException:同第二种,支持自定义时间单位

  4. boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者到了某个时间

针对Object的notify/notifyAll方法

  1. void signal():唤醒一个等待在condition上的线程,将该线程从等待队列中转移到同步队列中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。

  2. void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程

2.Condition实现原理分析

2.1 等待队列

要想能够深入的掌握condition还是应该知道它的实现原理,现在我们一起来看看condiiton的源码。创建一个condition对象是通过lock.newCondition(),而这个方法实际上是会new出一个ConditionObject对象,该类是AQS(AQS的实现原理的文章)的一个内部类,有兴趣可以去看看。

前面我们说过,condition是要和lock配合使用的也就是condition和Lock是绑定在一起的,而lock的实现原理又依赖于AQS,自然而然ConditionObject作为AQS的一个内部类无可厚非。

我们知道在锁机制的实现上,AQS内部维护了一个同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到同步队列,同样的,condition内部也是使用同样的方式,内部维护了一个 等待队列,所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。另外注意到ConditionObject中有两个成员变量:

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

这样我们就可以看出来ConditionObject通过持有等待队列的头尾指针来管理等待队列。主要注意的是Node类复用了在AQS中的Node类,其节点状态和相关属性可以去看AQS的实现原理的文章,如果您仔细看完这篇文章对condition的理解易如反掌,对lock体系的实现也会有一个质的提升。Node类有这样一个属性:

//后继节点
Node nextWaiter;

进一步说明,等待队列是一个单向队列,而在之前说AQS时知道同步队列是一个双向队列。接下来我们用一个demo,通过debug进去看是不是符合我们的猜想:

public static void main(String[] args) {
   for (int i = 0; i < 10; i++) {
       Thread thread = new Thread(() -> {
           lock.lock();
           try {
               condition.await();
           } catch (InterruptedException e) {
               e.printStackTrace();
           }finally {
               lock.unlock();
           }
       });
       thread.start();
   }
}

这段代码没有任何实际意义,甚至很臭,只是想说明下我们刚才所想的。新建了10个线程,没有线程先获取锁,然后调用condition.await方法释放锁将当前线程加入到等待队列中,通过debug控制当走到第10个线程的时候查看firstWaiter即等待队列中的头结点,debug模式下情景图如下:

Java并发专题【12】详解Condition的await和signal等待/通知机制

从这个图我们可以很清楚的看到这样几点:

1、调用condition.await方法后线程依次尾插入到等待队列中,如图队列中的线程引用依次为Thread-0,Thread-1,Thread-2....Thread-8;

2、等待队列是一个单向队列。通过我们的猜想然后进行实验验证,我们可以得出等待队列的示意图如下图所示:

Java并发专题【12】详解Condition的await和signal等待/通知机制

同时还有一点需要注意的是:我们可以多次调用lock.newCondition()方法创建多个condition对象,也就是一个lock可以持有多个等待队列。而在之前利用Object的方式实际上是指在对象Object对象监视器上只能拥有一个同步队列和一个等待队列,而并发包中的Lock拥有一个同步队列和多个等待队列。示意图如下:

Java并发专题【12】详解Condition的await和signal等待/通知机制

如图所示,ConditionObject是AQS的内部类,因此每个ConditionObject能够访问到AQS提供的方法,相当于每个Condition都拥有所属同步器的引用。

2.2 await实现原理

当调用condition.await()方法后会使得当前获取lock的线程进入到等待队列,如果该线程能够从await()方法返回的话一定是该线程获取了与condition相关联的lock。接下来,我们还是从源码的角度去看,只有熟悉了源码的逻辑我们的理解才是最深的。await()方法源码为:

public final void await() throws InterruptedException {
   if (Thread.interrupted())
       throw new InterruptedException();
 // 1. 将当前线程包装成Node,尾插入到等待队列中
   Node node = addConditionWaiter();
 // 2. 释放当前线程所占用的lock,在释放的过程中会唤醒同步队列中的下一个节点
   int savedState = fullyRelease(node);
   int interruptMode = 0;
   while (!isOnSyncQueue(node)) {
   // 3. 当前线程进入到等待状态
       LockSupport.park(this);
       if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
           break;
   }
 // 4. 自旋等待获取到同步状态(即获取到lock)
   if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
       interruptMode = REINTERRUPT;
   if (node.nextWaiter != null) // clean up if cancelled
       unlinkCancelledWaiters();
 // 5. 处理被中断的情况
   if (interruptMode != 0)
       reportInterruptAfterWait(interruptMode);
}

代码的主要逻辑请看注释,我们都知道当当前线程调用condition.await()方法后,会使得当前线程释放lock然后加入到等待队列中,直至被signal/signalAll后会使得当前线程从等待队列中移至到同步队列中去,直到获得了lock后才会从await方法返回,或者在等待时被中断会做中断处理。

那么关于这个实现过程我们会有这样几个问题:1. 是怎样将当前线程添加到等待队列中去的?2.释放锁的过程?3.怎样才能从await方法退出?而这段代码的逻辑就是告诉我们这三个问题的答案。

具体请看注释,在第1步中调用addConditionWaiter将当前线程添加到等待队列中,该方法源码为:

private Node addConditionWaiter() {
   Node t = lastWaiter;
   // If lastWaiter is cancelled, clean out.
   if (t != null && t.waitStatus != Node.CONDITION) {
       unlinkCancelledWaiters();
       t = lastWaiter;
   }
 //将当前线程包装成Node
   Node node = new Node(Thread.currentThread(), Node.CONDITION);
   if (t == null)
       firstWaiter = node;
   else
   //尾插入
       t.nextWaiter = node;
 //更新lastWaiter
   lastWaiter = node;
   return node;
}

这段代码就很容易理解了,将当前节点包装成Node,如果等待队列的firstWaiter为null的话(等待队列为空队列),则将firstWaiter指向当前的Node,否则,更新lastWaiter(尾节点)即可。

就是通过尾插入的方式将当前线程封装的Node插入到等待队列中即可,同时可以看出等待队列是一个不带头结点的链式队列,之前我们学习AQS时知道同步队列是一个带头结点的链式队列,这是两者的一个区别。

将当前节点插入到等待对列之后,会使当前线程释放lock,由fullyRelease方法实现,fullyRelease源码为:

final int fullyRelease(Node node) {
   boolean failed = true;
   try {
       int savedState = getState();
       if (release(savedState)) {
     //成功释放同步状态
           failed = false;
           return savedState;
       } else {
     //不成功释放同步状态抛出异常
           throw new IllegalMonitorStateException();
       }
   } finally {
       if (failed)
           node.waitStatus = Node.CANCELLED;
   }
}

这段代码就很容易理解了,调用AQS的模板方法release方法释放AQS的同步状态并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则正常返回,若失败的话就抛出异常。

到目前为止,这两段代码已经解决了前面的两个问题的答案了,还剩下第三个问题,怎样从await方法退出?现在回过头再来看await方法有这样一段逻辑:

while (!isOnSyncQueue(node)) {
 // 3. 当前线程进入到等待状态
   LockSupport.park(this);
   if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
       break;
}

很显然,当线程第一次调用condition.await()方法时,会进入到这个while()循环中,然后通过LockSupport.park(this)方法使得当前线程进入等待状态,那么要想退出这个await方法第一个前提条件自然而然的是要先退出这个while循环,出口就只剩下两个地方:1. 逻辑走到break退出while循环;2. while循环中的逻辑判断为false。

再看代码出现第1种情况的条件是当前等待的线程被中断后代码会走到break退出,第二种情况是当前节点被移动到了同步队列中(即另外线程调用的condition的signal或者signalAll方法),while中逻辑判断为false后结束while循环。

总结下,就是当前线程被中断或者调用condition.signal/condition.signalAll方法当前节点移动到了同步队列后 ,这是当前线程退出await方法的前提条件。当退出while循环后就会调用acquireQueued(node, savedState),这个方法在介绍AQS的底层实现时说过了,若感兴趣的话可以去看这篇文章,该方法的作用是在自旋过程中线程不断尝试获取同步状态,直至成功(线程获取到lock)。

这样也说明了退出await方法必须是已经获得了condition引用(关联)的lock。到目前为止,开头的三个问题我们通过阅读源码的方式已经完全找到了答案,也对await方法的理解加深。await方法示意图如下图:

Java并发专题【12】详解Condition的await和signal等待/通知机制

如图,调用condition.await方法的线程必须是已经获得了lock,也就是当前线程是同步队列中的头结点。调用该方法后会使得当前线程所封装的Node尾插入到等待队列中。

超时机制的支持

condition还额外支持了超时机制,使用者可调用方法awaitNanos,awaitUtil。这两个方法的实现原理,基本上与AQS中的tryAcquire方法如出一辙,关于tryAcquire可以仔细阅读这篇文章的第3.4部分。

不响应中断的支持

要想不响应中断可以调用condition.awaitUninterruptibly()方法,该方法的源码为:

public final void awaitUninterruptibly() {
   Node node = addConditionWaiter();
   int savedState = fullyRelease(node);
   boolean interrupted = false;
   while (!isOnSyncQueue(node)) {
       LockSupport.park(this);
       if (Thread.interrupted())
           interrupted = true;
   }
   if (acquireQueued(node, savedState) || interrupted)
       selfInterrupt();
}

这段方法与上面的await方法基本一致,只不过减少了对中断的处理,并省略了reportInterruptAfterWait方法抛被中断的异常。

2.3 signal/signalAll实现原理

调用condition的signal或者signalAll方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得lock。

按照等待队列是先进先出(FIFO)的,所以等待队列的头节点必然会是等待时间最长的节点,也就是每次调用condition的signal方法是将头节点移动到同步队列中。我们来通过看源码的方式来看这样的猜想是不是对的,signal方法源码为:

public final void signal() {
   //1. 先检测当前线程是否已经获取lock
   if (!isHeldExclusively())
       throw new IllegalMonitorStateException();
   //2. 获取等待队列中第一个节点,之后的操作都是针对这个节点
 Node first = firstWaiter;
   if (first != null)
       doSignal(first);
}

signal方法首先会检测当前线程是否已经获取lock,如果没有获取lock会直接抛出异常,如果获取的话再得到等待队列的头指针引用的节点,之后的操作的doSignal方法也是基于该节点。下面我们来看看doSignal方法做了些什么事情,doSignal方法源码为:

private void doSignal(Node first) {
   do {
       if ( (firstWaiter = first.nextWaiter) == null)
           lastWaiter = null;
   //1. 将头结点从等待队列中移除
       first.nextWaiter = null;
   //2. while中transferForSignal方法对头结点做真正的处理
   } while (!transferForSignal(first) &&
            (first = firstWaiter) != null);
}

具体逻辑请看注释,真正对头节点做处理的逻辑在transferForSignal放,该方法源码为:

final boolean transferForSignal(Node node) {
   /*
    * If cannot change waitStatus, the node has been cancelled.
    */
 //1. 更新状态为0
   if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
       return false;

   /*
    * Splice onto queue and try to set waitStatus of predecessor to
    * indicate that thread is (probably) waiting. If cancelled or
    * attempt to set waitStatus fails, wake up to resync (in which
    * case the waitStatus can be transiently and harmlessly wrong).
    */
 //2.将该节点移入到同步队列中去
   Node p = enq(node);
   int ws = p.waitStatus;
   if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
       LockSupport.unpark(node.thread);
   return true;
}

关键逻辑请看注释,这段代码主要做了两件事情1.将头结点的状态更改为CONDITION;2.调用enq方法,将该节点尾插入到同步队列中,关于enq方法请看AQS的底层实现这篇文章。

现在我们可以得出结论:调用condition的signal的前提条件是当前线程已经获取了lock,该方法会使得等待队列中的头节点即等待时间最长的那个节点移入到同步队列,而移入到同步队列后才有机会使得等待线程被唤醒,即从await方法中的LockSupport.park(this)方法中返回,从而才有机会使得调用await方法的线程成功退出。signal执行示意图如下图:

Java并发专题【12】详解Condition的await和signal等待/通知机制

signalAll

sigllAll与sigal方法的区别体现在doSignalAll方法上,前面我们已经知道doSignal方法只会对等待队列的头节点进行操作,,而doSignalAll的源码为:

private void doSignalAll(Node first) {
   lastWaiter = firstWaiter = null;
   do {
       Node next = first.nextWaiter;
       first.nextWaiter = null;
       transferForSignal(first);
       first = next;
   } while (first != null);
}

该方法只不过时间等待队列中的每一个节点都移入到同步队列中,即“通知”当前调用condition.await()方法的每一个线程。

3. await与signal/signalAll的结合思考

文章开篇提到等待/通知机制,通过使用condition提供的await和signal/signalAll方法就可以实现这种机制,而这种机制能够解决最经典的问题就是“生产者与消费者问题”,关于“生产者消费者问题”之后会用单独的一篇文章进行讲解,这也是面试的高频考点。

await和signal和signalAll方法就像一个开关控制着线程A(等待方)和线程B(通知方)。它们之间的关系可以用下面一个图来表现得更加贴切:

Java并发专题【12】详解Condition的await和signal等待/通知机制

如图,线程awaitThread先通过lock.lock()方法获取锁成功后调用了condition.await方法进入等待队列,而另一个线程signalThread通过lock.lock()方法获取锁成功后调用了condition.signal或者signalAll方法,使得线程awaitThread能够有机会移入到同步队列中,当其他线程释放lock后使得线程awaitThread能够有机会获取lock,从而使得线程awaitThread能够从await方法中退出执行后续操作。如果awaitThread获取lock失败会直接进入到同步队列。

4. 一个例子

我们用一个很简单的例子说说condition的用法:

public class AwaitSignal {
   private static ReentrantLock lock = new ReentrantLock();
   private static Condition condition = lock.newCondition();
   private static volatile boolean flag = false;

   public static void main(String[] args) {
       Thread waiter = new Thread(new waiter());
       waiter.start();
       Thread signaler = new Thread(new signaler());
       signaler.start();
   }

   static class waiter implements Runnable {

       @Override
       public void run() {
           lock.lock();
           try {
               while (!flag) {
                   System.out.println(Thread.currentThread().getName() + "当前条件不满足等待");
                   try {
                       condition.await();
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
               System.out.println(Thread.currentThread().getName() + "接收到通知条件满足");
           } finally {
               lock.unlock();
           }
       }
   }

   static class signaler implements Runnable {

       @Override
       public void run() {
           lock.lock();
           try {
               flag = true;
               condition.signalAll();
           } finally {
               lock.unlock();
           }
       }
   }
}

输出结果为:

Thread-0当前条件不满足等待
Thread-0接收到通知,条件满足

开启了两个线程waiter和signaler,waiter线程开始执行的时候由于条件不满足,执行condition.await方法使该线程进入等待状态同时释放锁,signaler线程获取到锁之后更改条件,并通知所有的等待线程后释放锁。这时,waiter线程获取到锁,并由于signaler线程更改了条件此时相对于waiter来说条件满足,继续执行。

猜你喜欢

转载自blog.csdn.net/hezuo1181/article/details/82948320