Java可重入锁ReentrantLock
前面已经介绍Java中的队列同步器——AQS,子类通过继承AQS并重写其指定方法tryAcquire()、tryRelease()、tryAcquireShared()、tryReleaseShared()和isHeldExclusively()即可管理同步状态,我们简单地理解"管理锁就是管理同步状态"。ReentrantLock通过静态内部类Sync,FairSync、NonfairSync继承AQS来实现锁的功能,所以队列同步器AQS的部分我们不再赘述,我们只重点分析ReentrantLock重写的几个方法,也就是 获取锁和释放锁的方法。
因此,在阅读本文章之前,应该先了解队列同步器AQS。
一、ReentrantLock的概念
ReentrantLock是一个可重入的独占锁(/互斥)锁。
- 可重入:指任意线程在获取到锁之后能够再次获取该锁而不会被阻塞。
- 独占:每次只能有一个线程能持有锁;与之相应的时共享锁,则允许多个线程同时获取锁,并发访问,共享资源,ReentrantReadWriteLock里的读锁,它的读锁是可以被共享的,但是它的写锁是独占的。
ReentrantLock继承了Lock接口,其内部类Sync继承了队列同步器AQS,Sync有两个子类:公平锁FairSync和非公平锁NonfairSync。在"公平锁"的机制下,线程依次排队获取锁;而"非公平锁"在锁是可获取状态时,不管自己是否在同步队列的队头都会获取锁。"公平锁"保证了锁的获取按照FIFO原则,而代价则是进行大量的线程切换,耗时多,开销大;"非公平锁"虽然可能导致线程饥饿,但却有极少的线程切换,保证了其更大的吞吐量。下面从源码的角度重点分析ReentrantLock的可重入、公平锁和非公平锁。
二、ReentrantLock的可重入
可重进入是指任意线程在获取到锁之后能够再次获取该锁而不会被锁阻塞,该特性的实现需要解决以下两个问题:
- 线程再次获取锁:锁需要去识别获取锁的线程是否为当前占据锁的线程,如果是,则再次成功获取,同步状态自增;
- 锁的最终释放:锁释放时,同步状态自减,线程重复 n 次获取了锁,需要进行 n 次释放锁,当同步状态等于 0时,锁释放成功,其它线程才能够获取到该锁。
所以只要分析清楚了ReentrantLock获取锁和释放锁的原理,就分析清楚了可重入。
三、获取锁
获取锁和释放锁的过程,其实就是获取同步状态和释放同步状态的过程。子类继承AQS之后,需要重写tryAcquire()、tryRelease()、tryAcquireShared()、tryReleaseShared()和isHeldExclusively()方法,它们的功能分别是 以独占方式获取锁、以独占释放锁、以共享方式获取锁、以独占方式释放锁 和 判断同步状态是否被当前线程独占。ReentrantLcok是独占锁,所以只需要重写tryAcquire()、tryRelease()和isHeldExclusively()方法即可。
ReentrantLock分为公平锁和非公平锁,它们对获取锁的方式不一样,也就是tryAcquire()方法的实现不一样,所以公平锁类FairSync和非公平锁NonfairSync要分别重写该方法;而它们释放锁的方式一样,所以tryRelease()方法在两者的公共父类Sync中重写,isHeldExclusively()方法也在Sync中重写。
3.1 公平锁
队列同步器AQS的内部维持着一个FIFO双向等待队列,公平锁的获取顺序符合FIFO原则,如果当前同步状态为0,需要调用hasQueuedPredecessors()方法判断同步队列中当前节点是否有前驱节点;如果当前同步状态不为0,需要判断已获取锁的线程是否为当前线程,如果是,则同步状态自增。公平锁FairSync的tryAcquire(int)方法如下所示:
1 /** 2 * 以公平方式获取锁 3 */ 4 protected final boolean tryAcquire(int acquires) { 5 // 获取当前线程 6 final Thread current = Thread.currentThread(); 7 // 获取同步状态 8 int c = getState(); 9 // 同步状态为0,表示没有线程获取锁 10 if (c == 0) { 11 if (!hasQueuedPredecessors() && 12 compareAndSetState(0, acquires)) { 13 setExclusiveOwnerThread(current); 14 return true; 15 } 16 } 17 // 同步状态不为0,表示已经有线程获取了锁,判断获取锁的线程是否为当前线程 18 else if (current == getExclusiveOwnerThread()) { 19 // 获取锁的线程是当前线程 20 int nextc = c + acquires; 21 if (nextc < 0) 22 throw new Error("Maximum lock count exceeded"); 23 setState(nextc); 24 return true; 25 } 26 return false; 27 }
hasQueuedPredecessors()方法主要是对同步队列中当前节点是否有前驱节点进行判断,如果该方法返回true,则表示有线程比当前线程更早地请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁,其源码如下所示:
1 public final boolean hasQueuedPredecessors() { 2 // The correctness of this depends on head being initialized 3 // before tail and on head.next being accurate if the current 4 // thread is first in queue. 5 // 同步队列尾节点 6 Node t = tail; // Read fields in reverse initialization order 7 // 同步队列头节点 8 Node h = head; 9 Node s; 10 return h != t && 11 ((s = h.next) == null || s.thread != Thread.currentThread()); 12 }
3.2 非公平锁
非公平锁的实现在Sync的nonfairTryAcquire()方法中,与公平锁比较,唯一不同就是非公平锁不需要进行hasQueuedPredecessors()判断,源码如下图所示:
1 /** 2 * 以非公平方式获取锁 3 */ 4 final boolean nonfairTryAcquire(int acquires) { 5 // 获取当前线程 6 final Thread current = Thread.currentThread(); 7 // 获取同步状态 8 int c = getState(); 9 // 同步状态为0,表示没有线程获取锁 10 if (c == 0) { 11 // 执行CAS操作,尝试修改同步状态 12 if (compareAndSetState(0, acquires)) { 13 // 同步状态修改成功,获取到锁 14 setExclusiveOwnerThread(current); 15 return true; 16 } 17 } 18 // 同步状态不为0,表示已经有线程获取了锁,判断获取锁的线程是否为当前线程 19 else if (current == getExclusiveOwnerThread()) { 20 // 获取锁的线程是当前线程 21 // 同步状态自增 22 int nextc = c + acquires; 23 if (nextc < 0) // overflow 24 throw new Error("Maximum lock count exceeded"); 25 setState(nextc); 26 return true; 27 } 28 // 获取锁的线程不是当前线程 29 return false; 30 }
四、释放锁
公平锁和非公平锁的释放锁的方法tryRelease()都自继承父类Sync,也就是tryRelease()方法在Sync重写。获取锁时,同步状态自增;释放锁时,同步状态自减;ReentrantLock是可重入锁,如果线程重复获取 n 次锁,就需要释放 n 次锁,即同步状态为0,这样释放锁才成功,其它等待的线程才可以获取同步状态,tryRelease()源码如下:
1 protected final boolean tryRelease(int releases) { 2 // 计算新的状态值 3 int c = getState() - releases; 4 // 判断当前线程是否是持有锁的线程,如果不是的话,抛出异常 5 if (Thread.currentThread() != getExclusiveOwnerThread()) 6 throw new IllegalMonitorStateException(); 7 boolean free = false; 8 // 新的状态值是否为0,若为0,则表示该锁已经完全释放了,其他线程可以获取同步状态了 9 if (c == 0) { 10 free = true; 11 setExclusiveOwnerThread(null); 12 } 13 // 更新状态值 14 setState(c); 15 return free; 16 }
五、ReentrantLock与synchronized的区别与联系
区别:
- ReentrantLock是JDK类层面实现;synchronized是JVM层面实现。
- ReentrantLock增加了一些高级功能,主要以下三项:等待可中断、可实现公平锁及可以绑定多个条件(一个ReentrantLock对象可以同时绑定多个Condition对象)。
联系:
- ReentrantLock与synchronized都是可重入锁,同一线程反复进入同步块也不会出现自己把自己锁死的情况。
JDK 6或以上版本,性能已经不再是选择synchronized或者ReentrantLock的决定因素。基于以下理由,我们仍然推荐在synchronized与ReentrantLock都可满足需要时优先使用synchronized:
- synchronized是在Java语法层面的同步,足够清晰,也足够简单。每个Java程序员都熟悉synchronized,但J.U.C中的Lock接口则并非如此。因此在只需要基础的同步功能时,更推荐synchronized。
- Lock应该确保在finally块中释放锁,否则一旦受同步保护的代码块中抛出异常,则有可能永远不会释放持有的锁。这一点必须由程序员自己来保证,而使用synchronized的话则可以由Java虚拟机来确保即使出现异常,锁也能被自动释放。
- 尽管在JDK 5时代ReentrantLock曾经在性能上领先过synchronized,但这已经是十多年之前的胜利了。从长远来看,Java虚拟机更容易针对synchronized来进行优化,因为Java虚拟机可以在线程和对象的元数据中记录synchronized中锁的相关信息,而使用J.U.C中的Lock的话,Java虚拟机是很难得知具体哪些锁对象是由特定线程锁持有的。