可重入排他锁ReentrantLock源码浅析
1.引子
"ReentrantLock"单词中的“Reentrant”就是“重入”的意思,正如其名,ReentrantLock是一个支持重入的排他锁,即同一个线程中可以多次获得同步状态,常表现为lock()方法的嵌套使用(类似于synchronized代码类嵌套),而在AQS类注释的使用说明中的Mutex是一个不可重入的锁,只要一个线程获得了同步状态,再次tryAcquire(int)返回false。
另外ReentrantLock还支持公平锁和非公平锁的的选择,公平锁是指等待时间长的线程优先获取锁,非公平锁则对所有线程一视同仁;synchronized关键字只支持非同步锁,这是由JVM的本地C++代码决定的。
公平锁虽能解决某些线程长久等待,减少“饥饿”的发生概率,但公平锁没有非公平锁的效率高,因为它要频繁地进行线程上下文切换,一般情况下使用非公平锁。ReentrantLock可以通过设置构造方法的参数来决定使用公平锁或非公平锁,其默认的无参构造方法创建的是一个非公平锁。
ReentrantLock与synchronized的区别对比在以前的帖子Lock接口简介中已经说明过了,这里不再赘述。
2.类结构
由类结构图可以看出,ReentrantLock类中有Sync、FairSync、NofairSync这三个静态内部类。Sync是一个继承于AQS的一个抽象类(AQS相关的内容在之前的帖子),它表示公平锁与非公平锁的通用或共同之处的抽象。FairSync 和NofairSync都继承自Sync,分别表示公平锁、非公平锁.它们两者的类定义差别很小,只有尝试获取锁的方法不同,FairSync使用自已定义的tryAcquire(int),而NotFairSync将tryAcquire(int)委托给父类Sync的nonfairTryAcquire(int)方法实现,而两者尝试释放锁的方法都是继承父类Sync的tryRelease(int)
|
ReentrantLock的构造方法
ReentrantLock有一个Sync类型的成员变量sync,这个成员变量在构造方法中被实例化。无参的构造方法,将sync实例化为一个NonfairSync对象,此时的ReentrantLock表示一个非公平锁。带一个布尔型参数的构造方法,根据参数就会创建相应的公平锁/非公平锁。
private final Sync sync; public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L; /** * Performs {@link Lock#lock}. The main reason for subclassing * is to allow fast path for nonfair version. */ abstract void lock(); /** * Performs non-fair tryLock. tryAcquire is implemented in * subclasses, but both need nonfair try for trylock method. */ 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) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } protected final boolean isHeldExclusively() { // While we must in general read state before owner, // we don't need to do so to check if current thread is owner return getExclusiveOwnerThread() == Thread.currentThread(); } final ConditionObject newCondition() { return new ConditionObject(); } // Methods relayed from outer class final Thread getOwner() { return getState() == 0 ? null : getExclusiveOwnerThread(); } final int getHoldCount() { return isHeldExclusively() ? getState() : 0; } final boolean isLocked() { return getState() != 0; } /** * Reconstitutes the instance from a stream (that is, deserializes it). */ private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); // reset to unlocked state } }
Sync的一些其他方法
//当前线程是独占线程 protected final boolean isHeldExclusively() { return getExclusiveOwnerThread() == Thread.currentThread(); } //给当前锁绑定一个新的条件 final ConditionObject newCondition() { return new ConditionObject(); } //获得锁的线程 final Thread getOwner() { return getState() == 0 ? null : getExclusiveOwnerThread(); } //锁被当前线程获取重复获取的次数 final int getHoldCount() { return isHeldExclusively() ? getState() : 0; } //锁否已经被某一线程获取到了 final boolean isLocked() { return getState() != 0; } /** * Reconstitutes the instance from a stream (that is, deserializes it). */ private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); // reset to unlocked state }
3.可重入的实现机制
锁的可重入是指,某线程在获取到锁之后还能获取到此锁而不会被此锁给阻塞。要保证这点要解决这两个方面的问题:
1) 线程再次获取锁。当要确定当前线程是否已经获取到了锁,如果是,那么再次获取锁也必须是成功的。代码的实现:每次获取锁将AQS的state属性自增,state表示锁被线程获取到的次数。
2) 锁释放。一线程获取了n次锁,那么也需要经过n次释放,锁才能完成审美观点释放,其他线程才能获取到此锁。代码实现思路:每次释放锁让AQS的state属性自减,当state为0时,表明锁被完全释放了。
非公平锁是默认实现,这里以非公平锁为例。
非公平锁获取锁lock()调用的底层方法nonfairTryAcquire(int)
/** * Performs non-fair tryLock. tryAcquire is implemented in * subclasses, but both need nonfair try for trylock method. */ final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) {//没有任何线程获取同步状态 if (compareAndSetState(0, acquires)) {//cas成功,当前线程获取同步状态成功 setExclusiveOwnerThread(current);//设置当前线程为锁的独占线程 return true; } } /** *当前线程是独占线程,即当前线程之前已经获取到了同步状态. * 进入重入处理 */ else if (current == getExclusiveOwnerThread()) { /** * nonfairTryAcquire(int)方法的被调用链上层是acquire(1),这里的acquires为1 * 相当于state自增 */ int nextc = c + acquires; if (nextc < 0) //nextc超过int类型表示的最大范围 throw new Error("Maximum lock count exceeded"); setState(nextc);//将state属性自增 return true; } return false; }
此方法的基本逻辑是:如果没有任何线程获取到此锁,尝试CAS尝试更新state,若此CAS更新成功,则成功获取锁并将当前线程设置为锁的占有线程,若CAS更新抢购,则获取锁失败。若当前线程获取之前已经获取到此锁,则将重复获取到锁的次数state自增。
非公平锁(和公平锁)释放锁的方法unlock()底层调用父类Sync的tryAcquire(int)
protected final boolean tryRelease(int releases) { int c = getState() - releases;//传入的参数releases为1 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException();//当前线程不是独占线程,即当前线程没获取到同步状态,怎么能释放同步状 boolean free = false; if (c == 0) { /** * 锁当前被重复获取的次数为0,锁已经被彻底释放了,其他线程能获取此锁了 */ free = true; setExclusiveOwnerThread(null); } setState(c);//state属性自减 return free; }
此方法的基本逻辑是:如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true。可以看到,该方法将同步状态是否为0作为最终释放的条件,当同步状态为0时,将占有线程设置为null,并返回true,表示释放成功。
4.公平锁与非公平锁
获取同步状态的所有线程都经AQS的静态内部类Node包装成一个Node对象,所有线程都在这由Node构建的先进先出的同步队列中。如果锁是公平锁,那么锁的获取顺序就应该符请求的绝对时间顺序,先请求锁的线程优先获取锁,即也就是先进先出。
公平锁获取锁调用的底层方法tryAcquire(int)
protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { /* *与nonfairTryAcquire(int)方法相比,此处有些不同, * 只是多了"hasQueuedPredecessors()",没有前驱节点,才进行CAS更新state。 * (每个节点代表一个线程)只有在没有其他线程比当前线程等待更久的情况下才尝试获取锁 */ 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(int)方法相似,只是多了”是否没有前驱节点的判断",只有在没有其他线程比当前线程等待更久的情况下当前线程才会尝试获取锁。
下面进行重入和(非)公平的相关测试
LockTest类中自定义一个锁CustomLock,它继承自ReentrantLock,与ReentrantLock相比,只添加一个"getQueuedThreadNames()"用来返回在同步队列中等待的线程名。
package juc; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.locks.ReentrantLock; public class LockTest { private static final CustomLock fairLock = new CustomLock(true); private static final CustomLock unfairLock = new CustomLock(false); public static void printText(boolean fair) { final CustomLock lock = fair ? fairLock : unfairLock; lock.lock(); try { Thread t = Thread.currentThread(); String pName = t.getName(); System.out.print("线程[" + pName + "]第1次重入。"); lock.lock(); try { System.out.print("线程[" + pName + "]第2次重入。"); System.out.println("阻塞的线程有" + lock.getQueuedThreadNames()); } finally { lock.unlock(); } } finally { lock.unlock(); } } public static void main(String[] args) { for (int i = 0; i < 5; i++) { //非公平锁测试 new Thread(() -> { LockTest.printText(false); LockTest.printText(false); LockTest.printText(false); }, "pid" + (i + 1)).start(); //公平锁测试 // new Thread(() -> { // LockTest.printText(true); // // LockTest.printText(true); // LockTest.printText(true); // // }, "pid" + (i + 1)).start(); } } private static class CustomLock extends ReentrantLock { CustomLock() { super(); } CustomLock(boolean fair) { super(fair); } List<String> getQueuedThreadNames() { Collection<Thread> threads = super.getQueuedThreads(); List<String> tNames = new ArrayList<>(threads.size()); threads.forEach((thread) -> tNames.add(thread.getName())); return tNames; } } }
控制台打印输出
非公平锁 | |
公平锁 |
可以明显看出:非公平性锁出现了一个线程连续获取锁的情况,而公平性锁每次都是从同步队列中的第一个节点获取到锁(等待时间最久的线程)。
非公平锁可能存在的问题:
在nonfairTryAcquire(int acquires)方法,当一个线程请求锁时,只要获取了同步状态即成功获取锁。在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。非公平锁可能造成某些线程总是连续获取到锁,而一些线程长期获取不到锁。虽然一些线程获取不到锁,会造成线程“饥饿”,但同一个线程连续获取锁,却减少了线程上下文切换造成的资源消耗,整体上能提高系统的吞吐量。
参考:《Java并发编程的艺术》方腾飞