并发编程学习笔记(十七、ReentrantLock源码分析)
目录:
- 什么是ReentrantLock
- ReentrantLock源码解析
- 总结
什么是ReentrantLock
ReentrantLock是可重入锁,指当一个线程获取某个对象时还能再次获得该对象,重入是指重入某对象的锁。
那什么时候会用到可重入锁呢?
1 public class Demo { 2 3 public synchronized void functionA(){ 4 System.out.println("iAmFunctionA"); 5 functionB(); 6 } 7 8 public synchronized void functionB(){ 9 System.out.println("iAmFunctionB"); 10 } 11 12 }
可重入锁的作用就是避免死锁,向内置锁synchronize与ReentrantLock都是可重入锁。
可重入锁的实现:
- 通过为每个锁关联一个请求计数器和一个获得该锁的线程。
- 当计数器为0时,认为锁是未被占用的。线程请求一个未被占用的锁时,JVM将记录该线程并将请求计数器设置为1,此时该线程就获得了锁,当该线程再次请求这个锁,计数器将递增。
- 当线程退出同步方法或者同步代码块时,计数器将递减,当计数器为0时,线程就释放了该对象,其他线程才能获取该锁。
参考:https://blog.csdn.net/startyangu/article/details/83933416
ReentrantLock源码解析
首先ReentrantLock实现了Lock接口,其内部有Sync、NonfairSync、FairSync这些类。
因Lock接口的定义之前说过,这里就不再赘述,你可以看下https://www.cnblogs.com/bzfsdr/p/13154793.html。
——————————————————————————————————————————————————————————————————————
1、内部类:
首先因为NonfairSync、FairSync都是Sync的子类,所以我们要先看看Sync。
1 /** 2 * 可重入锁的同步控制器 3 */ 4 abstract static class Sync extends AbstractQueuedSynchronizer { 5 private static final long serialVersionUID = -5179523762034025860L; 6 7 /** 8 * 加锁,由子类FairSync、NonfairSync来实现 9 */ 10 abstract void lock(); 11 12 /** 13 * 非公平的加锁方式 14 */ 15 final boolean nonfairTryAcquire(int acquires) { 16 final Thread current = Thread.currentThread(); 17 int c = getState(); 18 // c == 0表示没有线程持有该锁 19 if (c == 0) { 20 // CAS修改status状态,若成功则表示加锁成功 21 if (compareAndSetState(0, acquires)) { 22 // 设置获取到锁的线程 23 setExclusiveOwnerThread(current); 24 return true; 25 } 26 } 27 // 若c不是0,表示此锁已被占有;当前线程是拥有的的线程,进入如下逻辑 28 else if (current == getExclusiveOwnerThread()) { 29 // 发生重入,则增加重入的次数 30 int nextc = c + acquires; 31 // 若nextc < 0则表示发生了int类型的溢出 32 if (nextc < 0) // overflow 33 throw new Error("Maximum lock count exceeded"); 34 setState(nextc); 35 return true; 36 } 37 return false; 38 } 39 40 /** 41 * 对AQS中的tryRelease()重写 42 */ 43 protected final boolean tryRelease(int releases) { 44 int c = getState() - releases; 45 if (Thread.currentThread() != getExclusiveOwnerThread()) 46 throw new IllegalMonitorStateException(); 47 // 是否完全释放 48 boolean free = false; 49 // c == 0表示锁已经完全释放了,没有任何线程持有锁 50 if (c == 0) { 51 free = true; 52 setExclusiveOwnerThread(null); 53 } 54 setState(c); 55 return free; 56 } 57 58 /** 59 * 对AQS中的isHeldExclusively()重写 60 */ 61 protected final boolean isHeldExclusively() { 62 // While we must in general read state before owner, 63 // we don't need to do so to check if current thread is owner 64 return getExclusiveOwnerThread() == Thread.currentThread(); 65 } 66 67 final ConditionObject newCondition() { 68 return new ConditionObject(); 69 } 70 71 // Methods relayed from outer class 72 73 /** 74 * 返回锁的持有者,若未加锁返回null 75 */ 76 final Thread getOwner() { 77 return getState() == 0 ? null : getExclusiveOwnerThread(); 78 } 79 80 /** 81 * 返回重入锁重入的次数 82 */ 83 final int getHoldCount() { 84 return isHeldExclusively() ? getState() : 0; 85 } 86 87 final boolean isLocked() { 88 return getState() != 0; 89 } 90 91 /** 92 * 通过流,反序列化对象 93 */ 94 private void readObject(java.io.ObjectInputStream s) 95 throws java.io.IOException, ClassNotFoundException { 96 s.defaultReadObject(); 97 setState(0); // reset to unlocked state 98 } 99 }
Sync的实现很简单,你只要有AQS的基础以及了解我最开始说的可重入锁的原理的话,你可以很轻松的看懂。
还有一点注意下,上述第32行nextc < 0会内存溢出,两个整数相加为啥会溢出呢,这和Integer.MAX_VALUE + 1一个道理。
可见 https://blog.csdn.net/KAKE_yuze/article/details/84990237。
——————————————————————————————————————————————————————————————————————
然后我们再分别看看FairSync和NonfairSync。
1 static final class FairSync extends Sync { 2 private static final long serialVersionUID = -3000897897090466540L; 3 4 final void lock() { 5 // 调用了AQS中的acquire 6 acquire(1); 7 } 8 9 /** 10 * Fair version of tryAcquire. Don't grant access unless 11 * recursive call or no waiters or is first. 12 */ 13 protected final boolean tryAcquire(int acquires) { 14 final Thread current = Thread.currentThread(); 15 int c = getState(); 16 if (c == 0) { 17 // 与之前Sync中分析的nonfairTryAcquire很像,但加锁条件多了一个hasQueuedPredecessors 18 // hasQueuedPredecessors():用于查询是否有等待获取锁的时间比当前线程更长的线程 19 if (!hasQueuedPredecessors() && 20 compareAndSetState(0, acquires)) { 21 setExclusiveOwnerThread(current); 22 return true; 23 } 24 } 25 else if (current == getExclusiveOwnerThread()) { 26 int nextc = c + acquires; 27 if (nextc < 0) 28 throw new Error("Maximum lock count exceeded"); 29 setState(nextc); 30 return true; 31 } 32 return false; 33 } 34 }
首先我们可以看到FairSync的tryAcquire与Sync中分析的nonfairTryAcquire很像,但需要hasQueuedPredecessors()返回false的时候才能够加锁,那我们就着重分析下hasQueuedPredecessors()。
1 /** 2 * 用于查询是否有等待获取锁的时间比当前线程更长的线程 3 */ 4 public final boolean hasQueuedPredecessors() { 5 // The correctness of this depends on head being initialized 6 // before tail and on head.next being accurate if the current 7 // thread is first in queue. 8 Node t = tail; // Read fields in reverse initialization order 9 Node h = head; 10 Node s; 11 return h != t && 12 ((s = h.next) == null || s.thread != Thread.currentThread()); 13 }
首先我们知道因为队列是先进先出的,所以离队列头部更近的排队时间肯定会更长些,根据这个思路我们来分析下上述代码。
h != t,也就是队列包含至少两个元素是返回true,反之队列为空或仅有一个元素的时候这段逻辑会返回false。
然后我们看下第二段:(s = h.next) == null || s.thread != Thread.currentThread()
- (s = h.next) == null:第二个元素为null,返回true,反之false。
- s.thread != Thread.currentThread():s.thread不为当前线程返回true,若是当前线程返回false。
结合队列的特性我们在看下hasQueuedPredecessors()的注释:用于查询是否存在等待获取锁时间比当前线程更长的线程。
——————————————————————————————————————————————————————————————————————
1 static final class NonfairSync extends Sync { 2 private static final long serialVersionUID = 7316153563782823691L; 3 4 /** 5 * 加锁 6 */ 7 final void lock() { 8 // CAS使state从0变成1,若CAS成功则将当前线程置为锁的持有者 9 if (compareAndSetState(0, 1)) 10 setExclusiveOwnerThread(Thread.currentThread()); 11 else 12 // CAS失败,则调用AQS的acquire方法进行加锁 13 acquire(1); 14 } 15 16 protected final boolean tryAcquire(int acquires) { 17 // 调用父类Sync的nonfairTryAcquire 18 return nonfairTryAcquire(acquires); 19 } 20 }
——————————————————————————————————————————————————————————————————————
2、属性与构造器:
ReentrantLock只有一个属性,也就是其内部类Sync:
1 private final Sync sync;
构造器也很简单,默认是非公平锁;你也可以使用有参的构造,true是公平锁,false是非公平锁。
1 public ReentrantLock() { 2 sync = new NonfairSync(); 3 } 4 5 public ReentrantLock(boolean fair) { 6 sync = fair ? new FairSync() : new NonfairSync(); 7 }
3、公平锁、非公平锁的加锁解锁解析:
1 /** 2 * 加锁 3 */ 4 public void lock() { 5 sync.lock(); 6 } 7 8 /** 9 * 解锁 10 */ 11 public void unlock() { 12 sync.release(1); 13 }
加锁、解锁都是通过属性sync来控制,可是公平锁,也可是非公平锁,取决于你的构造函数的使用。
其实现上面已经说过,你可自行查阅一番。
总结
公平锁与非公平锁两者没有具体的好坏之分,根据具体的使用场景选择对应的锁技术。
- 公平锁:侧重的是公平性。
- 非公平锁:侧重的是并发性。
非公平锁对锁的竞争是抢占式的(队列中线程除外)线程在进入等待队列前可以进行两次尝试这大大增加了获取锁的机会。
这种好处体现在两个方面:
- 线程不必加入等待队列就可以获得锁,不仅免去了构造结点并加入队列的繁琐操作同时也节省了线程阻塞唤醒的开销线程阻塞和唤醒涉及到线程上下文的切换和操作系统的系统调用是非常耗时的。在高并发情况下如果线程持有锁的时间非常短,短到线程入队阻塞的过程超过线程持有并释放锁的时间开销那么这种抢占式特性对并发性能的提升会更加明显。
- 减少CAS竞争。如果线程必须要加入阻塞队列才能获取锁那入队时CAS竞争将变得异常激烈,CAS操作虽然不会导致失败线程挂起,但不断失败重试导致的对CPU的浪费也不能忽视。