并发编程实践五:ReentrantLock

来源:互联网 发布:海鹰数据网 编辑:程序博客网 时间:2024/05/21 07:55

ReentrantLock是一个可重入的互斥锁,实现了接口Lock,和synchronized相比,它们提供了相同的功能,但ReentrantLock使用更灵活,功能更强大,也更复杂。这篇文章将为你介绍ReentrantLock,以及它的实现机制。

ReentrantLock介绍

通常,ReentrantLock按下面的方式使用:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class ReentrantLockTest {  
  2.     private final ReentrantLock lock = new ReentrantLock();//问题1:lock为什么定义为final  
  3.   
  4.     public void m() {  
  5.         lock.lock();  
  6.         try {  
  7.             // method body  
  8.         } finally {  
  9.             lock.unlock();  
  10.         }  
  11.     }  
  12. }  

首先需要定义一个lock,在使用时首先通过lock的lock方法加锁,然后执行临界区代码,最后在final中调用lock的unlock方法解锁(防止异常后无法解锁)。想了解锁的实现原理,可以参考上一篇:“并发编程实践四:实现正确和高效的锁”。
ReentrantLock提供了两种锁:公平锁和非公平锁,默认是非公平锁。若指定为公平锁,则所有线程尽量按照调用lock的先后次序获取锁(问题二:为什么说尽量?),否则,如果为非公平锁,则调用lock的线程和等待队列中的线程将竞争锁。公平锁更加公平,但非公平锁则具有更好的性能。
ReentrantLock是可重入的,也就是一个线程可以多次调用lock成功,但要求调用了多少次lock,就需要对应调用多少次unlock,并且该锁最多支持同一个线程发起的2147483648(锁的数量是用一个int变量保存)个递归锁,超出这个限制将会导致lock方法抛出error。
ReentrantLock除了实现Lock接口外,还提供了一些辅助的方法,如:isLocked和getQueueLength等,这些方法对检测和监视可能很有用。
下面我将对这些功能的内部实现做详细的介绍。

ReentrantLock实现

ReentrantLock内部使用了一个AQS的实现类,我在“并发编程实践二:AbstractQueuedSynchronizer”中对AQS的基本流程做过一个基本的介绍,并涉及到一些代码细节,不过不了解AQS也不会影响对这篇文章的理解。ReentrantLock使用了AQS的互斥模式,下面我将分别介绍非公平锁、公平锁和ReentrantLock提供的辅助功能。

非公平锁

你可以使用下面的方法定义一个公平锁:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. private final ReentrantLock lock = new ReentrantLock(false);或者直接  
  2. private final ReentrantLock lock = new ReentrantLock();  

线程首先通过ReentrantLock的lock来申请锁,ReentrantLock的lock调用NonfairSync(AQS的实现类)的lock方法。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. //NonfairSync的lock  
  2. final void lock() {  
  3.     if (compareAndSetState(01))  
  4.         setExclusiveOwnerThread(Thread.currentThread());  
  5.     else  
  6.         acquire(1);  
  7. }  

NonfairSync的lock方法首先尝试更改AQS的状态(这里也就是新到的线程和等待队列中的线程竞争获取锁,新到的线程可能会获得成功,导致不公平),如果更改成功则修改当前锁的owner为自己,然后返回。否则进入acquire。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public final void acquire(int arg) {  
  2.     if (!tryAcquire(arg) &&  
  3.         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  
  4.         selfInterrupt();  
  5. }  

acquire调用tryAcquire来再次尝试获取锁,如果成功,则返回,否则调用addWaiter将自己加入等待队列,最后在acquireQueued中等待唤醒和执行唤醒后的操作。从tryAcquire开始:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. protected final boolean tryAcquire(int acquires) {  
  2.     return nonfairTryAcquire(acquires);  
  3. }  
  4. final boolean nonfairTryAcquire(int acquires) {  
  5.     final Thread current = Thread.currentThread();  
  6.     int c = getState();  
  7.     if (c == 0) {//锁空闲  
  8.         if (compareAndSetState(0, acquires)) {//尝试获取锁  
  9.             setExclusiveOwnerThread(current);//设置锁owner  
  10.             return true;  
  11.         }  
  12.     }  
  13.     else if (current == getExclusiveOwnerThread()) {//owner是自己  
  14.         int nextc = c + acquires;  
  15.         if (nextc < 0// 溢出,由于state是一个int,因此最多只能申请2147483648次  
  16.             throw new Error("Maximum lock count exceeded");  
  17.         setState(nextc);  
  18.         return true;  
  19.     }  
  20.     return false;  
  21. }  

tryAcquire直接调用nonfairTryAcquire,nonfairTryAcquire首先获取AQS状态,如果状态为0,则说明当前锁已经空闲,则再次尝试更改状态,如果成功,则将锁的owner设置为自己,然后返回true,失败则返回false;如果AQS状态不为0,则说明锁已经被占用,如果owner是自己,则可以再次获取锁,如果锁已经溢出,则报错,否则设置AQS状态,返回true;不符合上述情况,返回false。
tryAcquire失败后,线程进入addWaiter将自己加入等待队列。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. private Node addWaiter(Node mode) {  
  2.     Node node = new Node(Thread.currentThread(), mode);  
  3.     Node pred = tail;  
  4.     if (pred != null) {//队列已经初始化  
  5.         node.prev = pred;  
  6.         if (compareAndSetTail(pred, node)) {//尝试  
  7.             pred.next = node;  
  8.             return node;  
  9.         }  
  10.     }  
  11.     enq(node);  
  12.     return node;  
  13. }  
  14. private Node enq(final Node node) {  
  15.     for (;;) {  
  16.         Node t = tail;  
  17.         if (t == null) { //初始化队列  
  18.             if (compareAndSetHead(new Node()))  
  19.                 tail = head;  
  20.         } else {  
  21.             node.prev = t;  
  22.             if (compareAndSetTail(t, node)) {  
  23.                 t.next = node;  
  24.                 return t;  
  25.             }  
  26.         }  
  27.     }  
  28. }  

addWaiter中,如果tail不为空,则将tail通过CAS设置为当前线程节点,如果成功,则返回;否则将进入enq中循环添加节点到tail,直到成功。enq中,如果tail为空,则应该是首次使用队列,需要初始化,则将队列的head设置为一个空节点,如果成功,则将tail等于head,否则,如果失败,则说明有其它线程已经初始化了head,进入下一个循环重新开始。若队列不为空,则更改tail为当前节点,循环直到成功。
在线程将自己添加到等待队列后,线程则进入acquireQueued中。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. final boolean acquireQueued(final Node node, int arg) {  
  2.     boolean failed = true;  
  3.     try {  
  4.         boolean interrupted = false;  
  5.         for (;;) {  
  6.             final Node p = node.predecessor();  
  7.             if (p == head && tryAcquire(arg)) {  
  8.                 setHead(node);  
  9.                 p.next = null// help GC  
  10.                 failed = false;  
  11.                 return interrupted;  
  12.             }  
  13.             if (shouldParkAfterFailedAcquire(p, node) &&  
  14.                 parkAndCheckInterrupt())  
  15.                 interrupted = true;  
  16.         }  
  17.     } finally {  
  18.         if (failed)  
  19.             cancelAcquire(node);  
  20.     }  
  21. }  

acquireQueued中首先进行一个检查,如果当前节点的前续节点为head(说明当前节点已经为队列的第一个节点),则再次调用tryAcquire尝试获取锁(这个尝试是必须的,因为其它线程可能在该线程入队列之前已经释放了锁,如果不再次尝试,可能导致线程长时间等待),成功或则更改head(节点出队列),然后返回。如果当前节点的前续节点不为head,则首先在shouldParkAfterFailedAcquire中检查并更改前续节点状态,然后在parkAndCheckInterrupt中进入阻塞睡眠。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {  
  2.     int ws = pred.waitStatus;  
  3.     if (ws == Node.SIGNAL)  
  4.         return true;  
  5.     if (ws > 0) {  
  6.         do {  
  7.             node.prev = pred = pred.prev;  
  8.         } while (pred.waitStatus > 0);  
  9.         pred.next = node;  
  10.     } else {  
  11.         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  
  12.     }  
  13.     return false;  
  14. }  

shouldParkAfterFailedAcquire中需要判断pred的waitStatus,如果为Node.SIGNAL(表示其它线程释放锁后,会唤醒pred的后续节点的线程),则返回true,线程将在parkAndCheckInterrupt中进入阻塞睡眠;否则如果ws大于0(表示pred已经被取消),则将已经取消的节点删除,并返回false(可能node已经是队列的第一个节点,返回false将导致线程在acquireQueued中再次尝试获取锁,如果获取锁失败将再次进入shouldParkAfterFailedAcquire中);否则线程将尝试将pred的值设置为Node.SIGNAL,并返回false(返回false将导致在acquireQueued中再次尝试获取锁,这一点非常重要,因为释放锁的线程只有在pred的waitStatus为Node.SIGNAL时,才会执行唤醒线程的操作,而在这里将pred的waitStatus设置为Node.SIGNAL之前,可能其它线程已经释放了锁,如果不再尝试一次获取锁,可能会导致线程长时间阻塞,因此,在pred的waitStatus设置成功后,必须要重新再尝试一次)。
当pred的waitStatus为Node.SIGNAL后,则线程在parkAndCheckInterrupt中进入阻塞睡眠。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. private final boolean parkAndCheckInterrupt() {  
  2.     LockSupport.park(this);  
  3.     return Thread.interrupted();  
  4. }  

线程进入阻塞睡眠后,就需要另一个线程在释放了锁之后将其唤醒,另一个线程会调用lock的unlock方法。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public void unlock() {  
  2.     sync.release(1);  
  3. }  

unlock最终调用AQS的release方法。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public final boolean release(int arg) {  
  2.     if (tryRelease(arg)) {  
  3.         Node h = head;  
  4.         if (h != null && h.waitStatus != 0)  
  5.             unparkSuccessor(h);  
  6.         return true;  
  7.     }  
  8.     return false;  
  9. }  

release中线程使用tryRelease释放锁,释放锁成功后将进入唤醒等待线程的流程:如果队列不为空,并且head的waitStatus不为0(表示存在后续节点的线程等待被唤醒),则调用unparkSuccessor唤醒后续节点的线程。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. protected final boolean tryRelease(int releases) {  
  2.     int c = getState() - releases;  
  3.     if (Thread.currentThread() != getExclusiveOwnerThread())//限制其它线程进入  
  4.         throw new IllegalMonitorStateException();  
  5.     boolean free = false;  
  6.     if (c == 0) {  
  7.         free = true;  
  8.         setExclusiveOwnerThread(null);  
  9.     }  
  10.     setState(c);  
  11.     return free;  
  12. }  

tryRelease中获取AQS的状态,并减去releases(释放锁),得到c,如果执行的线程不是锁的owner,则抛出异常(这里就限制了后面的代码只有锁的owner线程能够进入),如果c为0,则表示锁已经被释放(如果线程获取了多次锁,则需要unlock多次后锁才能释放),将锁的owner设置为空,然后设置AQS的状态到c,返回锁是否已经释放。
释放锁成功后,线程将在unparkSuccessor中唤醒等待队列中的线程。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. private void unparkSuccessor(Node node) {  
  2.     int ws = node.waitStatus;  
  3.     if (ws < 0)  
  4.         compareAndSetWaitStatus(node, ws, 0);  
  5.     Node s = node.next;  
  6.     if (s == null || s.waitStatus > 0) {  
  7.         s = null;  
  8.         for (Node t = tail; t != null && t != node; t = t.prev)  
  9.             if (t.waitStatus <= 0)  
  10.                 s = t;  
  11.     }  
  12.     if (s != null)  
  13.         LockSupport.unpark(s.thread);  
  14. }  

unparkSuccessor中,首先将node的waitStatus设置到0(node不再需要唤醒后续节点了),然后删除掉已经取消的节点,将最终有效的节点保存到s,如果s不为空,则执行唤醒操作。
线程执行完唤醒操作后,就退出结束了,然后唤醒的线程将重新进入acquireQueued中执行。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. final boolean acquireQueued(final Node node, int arg) {  
  2.     boolean failed = true;  
  3.     try {  
  4.         boolean interrupted = false;  
  5.         for (;;) {  
  6.             final Node p = node.predecessor();  
  7.             if (p == head && tryAcquire(arg)) {  
  8.                 setHead(node);  
  9.                 p.next = null// help GC  
  10.                 failed = false;  
  11.                 return interrupted;  
  12.             }  
  13.             if (shouldParkAfterFailedAcquire(p, node) &&  
  14.                 parkAndCheckInterrupt())  
  15.                 interrupted = true;  
  16.         }  
  17.     } finally {  
  18.         if (failed)  
  19.             cancelAcquire(node);  
  20.     }  
  21. }  

acquireQueued中,唤醒的线程的节点的前续肯定为head,线程将调用tryAcquire尝试获取锁(唤醒的线程将和新到的线程一起竞争锁),如果获取锁成功,则修改head(出队列),并退出;否则将重新进入阻塞状态(是不是很郁闷)。
到这里,整个的流程就结束了,下面我们来看看公平锁。

公平锁

公平锁和非公平锁流程大致相同,只是对新到的线程的处理上不一样,非公平锁是新到的线程和等待队列中的线程一起竞争锁,但公平锁则始终保证等待最长的线程获取锁。
公平锁的定义方式为:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. private final ReentrantLock lock = new ReentrantLock(true);  

公平锁和非公平锁的差异在于锁的获取上,公平锁的lock方法如下:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. final void lock() {  
  2.     acquire(1);  
  3. }  

不像非公平锁直接尝试获取锁,公平锁不尝试获取锁,直接进入acquire,这里acquire的操作和非公平锁是一致的,区别在tryAcquire上。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. protected final boolean tryAcquire(int acquires) {  
  2.     final Thread current = Thread.currentThread();  
  3.     int c = getState();  
  4.     if (c == 0) {  
  5.         if (!hasQueuedPredecessors() &&  
  6.             compareAndSetState(0, acquires)) {  
  7.             setExclusiveOwnerThread(current);  
  8.             return true;  
  9.         }  
  10.     }  
  11.     else if (current == getExclusiveOwnerThread()) {//owner是自己  
  12.         int nextc = c + acquires;  
  13.         if (nextc < 0)  
  14.             throw new Error("Maximum lock count exceeded");  
  15.         setState(nextc);  
  16.         return true;  
  17.     }  
  18.     return false;  
  19. }  

tryAcquire中公平锁在锁空闲(c==0)的情况下,首先通过hasQueuedPredecessors判断是否有等待线程,如果没有,才尝试获取锁,若获取锁成功,则将自己设置为锁的owner,并返回;如果锁不空闲,如果自己是锁的owner,则可以再次获取锁,否则返回false。
因此公平锁和非公平锁的区别就在于多了hasQueuedPredecessors判断。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public final boolean hasQueuedPredecessors() {  
  2.     Node t = tail;  
  3.     Node h = head;  
  4.     Node s;  
  5.     return h != t &&  
  6.         ((s = h.next) == null || s.thread != Thread.currentThread());  
  7. }  

hasQueuedPredecessors中,如果tail和head不同,并且head的next为空或者head的next的线程不是当前线程,则表示队列不为空。有两种情况会导致h的next为空:
     1)当前线程进入hasQueuedPredecessors的同时,另一个线程已经更改了tail(在enq中),但还没有将head的next指向自己,这中情况表明队列不为空;
     2)当前线程将head赋予h后,head被另一个线程移出队列,导致h的next为空,这种情况说明锁已经被占用。

如果队列不为空(hasQueuedPredecessors返回true),则tryAcquire返回false,线程将进入等待队列(后面的流程和非公平锁一致)。
由于线程的调度,非公平锁在判断的过程中可能出现:
 线程A调用tryAcquire失败后,并在调用addWaiter之前,线程B释放了锁,且线程C判断到锁空闲,进入hasQueuedPredecessors返回false(等待队列为空),最终C比A先获取到锁。
由此来看,公平锁也并非绝对公平。
并且,公平锁在使用中,后来的线程总是需要进入等待队列等待,会导致效率降低,从JDK文档的描述,效率将降低很多。

结束语

这篇文章主要介绍了ReentrantLock的公平锁和非公平锁的实现流程,公平锁尽量保证获取锁的公平性,采用先来先得的原则,但由于线程的调度,会导致某些后到的线程先获取到锁;非公平锁不保证锁的获取的公平性,新到的线程将和等待队列中的线程竞争锁。公平锁具备公平性但性能差,而非公平锁不保证公平性但具有更好的性能。

0 0
原创粉丝点击