B9 Concurrent 重入锁(ReentrantLock)
【概述】
java.util.concurrent.locks.ReentrantLock 实现 java.util.concurrent.locks.Lock 接口,加锁(lock)和 解锁(unlock)方法都基于 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer)实现,AQS 是基于 sun.misc.Unsafe 类的 CAS算法相关方法实现的。
【代码实例】
import java.util.concurrent.locks.ReentrantLock; public class Main { public static void main(String[] args) { Command c = new Command(); int nThreads = 5; for(int i = 0; i < nThreads; i++){ new Thread(c).start(); } } } class Command implements Runnable { ReentrantLock lock = new ReentrantLock(); @Override public void run() { try{ lock.lock(); System.out.println(Thread.currentThread().getName() + ": 获得锁!"); }catch(Exception e){ //处理异常 }finally{ System.out.println(Thread.currentThread().getName() + ": 释放锁!"); lock.unlock(); } } }
打印结果: 一个线程独占锁(排他锁),释放锁后其他的线程才能获得锁。
【ReentrantLock 初始化】
1). 首先来看下 ReentrantLock 的属性定义:
ReentrantLock 的功能主要依靠定义的这个 Sync 类型的变量 sync 来实现。Sync 是 ReentrantLock 创建的一个静态内部类,它继承了 AQS(java.util.concurrent.locks.AbstractQueuedSynchronizer),即 ReentrantLock 的主要功能是依靠 AQS 实现的。
2). ReentrantLock 的构造器
ReentrantLock 提供了两种构造器,主要功能是对变量 sync 进行初始化,提供了 FairSync 和 NonFairSync两种实现,从命名可以看出分别对应公平锁和非公平锁的实现;其中 fair 为 true 时初始化为 FairSync(公平锁),fair 为 false 时初始化为 NonfairSync(非公平锁);无参构造器默认初始化为 NonfairSync。所谓 “公平” 在于是否按照线程申请锁的顺序获得锁,FairSync(公平锁)使用队列实现首先申请锁的先获得锁(FIFO),增加了额外的队列操作开销;相对而言,NonfairSync(非公平锁)的并发效能更高,吞吐量更高。
【AQS 初始化】
1). AQS 的实现依靠 sun.misc.Unsafe 类中 CAS 算法相关的方法,故这里初始化了 Unsafe 对象 unsafe,以及相关变量的内存偏移量(offset),用于后面的 CAS 操作。
2). AQS 属性介绍:AQS 使用双向链表的数据结构存储请求锁的线程,所以设计了一个节点类 Node,Node 类除了双向链表的前继指针(volatile Node prev)和后继指针(volatile Node next),存储数据包括 线程对象(volatile Thread thread)、等待状态(volatile int waitStatus)、下一个等待指针(Node nextWaiter)。一个节点表示一个线程。
下面是一个关于 waitStatus 的值说明:
AQS 除了双向链表的首节点(volatile Node head) 和 尾节点(volatile Node tail)外,还有一个状态属性(volatile int state)用于表示锁的状态。如果当前没有线程获得锁,state 的值为 0;若当前已经有一个线程获得锁,由于锁可重入,每获得一次锁加数加 1。
下面代码展示了重入锁的实现,打印了线程重新获得同一把锁时,state 的值变化。
import java.util.concurrent.locks.ReentrantLock; public class Main{ public static void main(String[] args){ Command c = new Command(); int nThreads = 5; for(int n = 0; n < nThreads; n++){ new Thread(c).start(); } } } class Command implements Runnable{ ReentrantLock lock = new ReentrantLock(); @Override public void run() { try{ lock.lock(); run2(); }catch(Exception e){ }finally{ lock.unlock(); } } public void run2(){ try{ lock.lock(); System.out.println(Thread.currentThread().getName() + ":" + lock.getHoldCount()); }catch(Exception e){ }finally{ lock.unlock(); } } }
打印结果:
- getHoldCount 方法返回了当前线程获得锁的数量,如果当前线程没有获得锁则返回 0。可以看到这里调用的是变量 sync 的 getHoldCount() 方法。
- 这里需要判断当前线程是否获得锁,即是否为独占锁的线程,是则调用 getState() 方法返回获得锁的数量,否则返回 0。
- AQS 继承了 java.util.concurrent.locks.AbstractOwnableSynchronizer,该类存储了独占锁的线程的值。
- AQS 中的 getState() 方法返回的就是 state 的值。
【加锁操作 lock()】
- lock() 的用途是当前线程尝试去获得锁,如果当前锁没有被其他线程持有,则当前线程可以立即获得锁,设置持有锁数量为 1;如果当前线程已经持有锁,再次调用 lock() 方法时会在原来持有锁数量上加 1;如果锁被其他线程持有,则当前线程保持休眠状态直到锁的持有数被设置为 1 的时候,才会被激活尝试去获得锁。
首先来看下,NonfairSync(非公平锁)的实现:
- compareAndSetState(0, 1) 是去获得锁,如果返回 true, 则获得锁成功,否则获得锁失败。这个方法是 AQS 中的方法,使用 Unsafe 类的 CAS 方法 compareAndSwapInt 尝试将 state 的值从 0 更新为 1。如果当前没有任何线程持有锁,state 为 0,则 CAS 成功,返回 true。注意 state 使用 volatile 修饰,state 的值被当前线程修改后,立即从工作内存刷新回主内存,其他线程可见最新的修改。
setExclusiveOwnerThread(Thread.currentThread()); 则是将当前线程设置独占锁的线程。
- 如果锁已经被持有(state != 0),持有线程可能是当前线程,也可能是其他线程。执行 acquire(1);
- tryAcquire 方式是锁已经被持有的情况下,尝试去获得锁。
获取锁的最新状态,通过是否为 0 判断锁是否被持有,如果没有被持有,跟上面一样的操作,通过 CAS 操作尝试去获得锁,如果获得锁成功则设置当前线程为独占锁的线程,返回 true;如果当前锁已被持有,判断独占锁的线程是否为当前线程,为当前线程则累加锁的数量,修改 state 变量,返回 true。例如一个线程持有锁的数量为 5,则 state 的值为 5。如果当前线程无法获得锁,则返回 false。
- 如果 tryAcquire 返回 false, 则添加到独占锁等待队列中(addWaiter(Node.EXCLUSIVE))
新建了一个 Node 节点 node,设置线程为当前线程,模式为独占锁(Node.EXCLUSIVE)
这里创建了一个 CHL 队列锁,队列中的元素通过自旋操作尝试去获得锁。在队列中的节点按照 FIFO 的规则,越先进入队列的节点对应的线程越先获得锁。
- 创建完 Node 节点 node 并加入 CHL 队列后,尝试通过队列获得锁。这里是一个自旋操作,判断当前节点 node 的前继节点(node.prev)是否为 head 节点,如果是,则使用 tryAcquire() 方法去尝试获得锁。
- 成功获得锁后,通过 setHead(node) 将 head 节点设置为 node,并将 node 节点的 prev(前继节点) 和 thread(存储线程)设置为 null。设置 failed 变量为 false,并将 interrupted(是否中断)返回。
- 每一次自旋操作,如果获取锁失败,则线程可以shouldParkAfterFailedAcquire 判断 pred (node 的前继节点)是否已经被阻塞。
Node.SIGNAL : 值为 -1,waitStatus(等待状态) 设置为这个值说明节点线程已经要求其他的线程去将它唤醒(LockSupport#unpark),所以可以安全地对它进行阻塞(LockSupport#park),返回 true,其他情况都返回 false。
Node.CANCEL:值为 1,唯一一个大于 0 的值,设置为这个值说明当前 pred (node 的前继节点)已经取消获得锁的操作,应该继续查找其前继节点(ws != Node.CANCEL)作为 node 的前继节点。
其他情况,会调用 compareAndSetWaitStatus 方法将 pred (node 的前继节点)的 waitStatus(等待状态)更新为 NODE.SIGNAL。
- 如果 shouldParkAfterFailedAcquire 返回 true, 即 node 的 前继节点已经被阻塞,则对当前线程进行 park 操作,并检查是否中断。这里使用 LockSupport#park() 方法进行阻塞线程。通过 Thread.interrupted() 判断线程是否被中断,并重置中断状态。如果线程被中断,则该方法返回 true, acquireQueued 方法中设置 interrupted 为 true。
- 线程被重置中断后,返回是否中断为 true, 故在 acquire 方法中执行了 selfInterrupt 进行线程中断。
- 最后在 finally 块中判断节点线程是否获得锁成功(failed = false),如果失败(failed = true),则执行 cancelAcquire 操作。(这里的代码不细说)
- 接下来看 FairSync(公平锁)如何实现 lock(加锁操作)
对比 NonFairSync(非公平锁),NonFairSync 会尝试先去获得锁,即后申请锁的线程有可能优先获得锁;而 FairSync(公平锁)则按照 FIFO 的原则让先申请锁的线程去获得锁。
- 可以看到 FairSync(公平锁)的 tryAcquire 方法中与 NonFairSync(非公平锁)方法不同的是,它会先去进行 hasQueuedPredecessors 方法判断当前队列是否有线程正在进行获得锁操作。若返回 true,则有其他线程正在进行获得锁操作,当前线程不再进行获得锁操作,tryAcquire 方法返回 false。
- 然后同样进入队列,执行与 NonfairSync(非公平锁)一样的操作
【尝试获得锁 tryLock()】
- tryLock() 表示当前线程尝试去获得锁,若获得锁成功,则返回 true;若获得锁失败,则返回 false。
- 这里用到的 nonfairTryAcquire 在上面 NonfairSync(非公平锁)实现方法中已经讲过了,但它属于 AQS 的部分,不属于 NonfairSync 的实现。可以看到 tryLock() 只会尝试一次去获得锁,并没有加入 CHL 锁队列中。
【可以抛出异常的加锁方法 lockInterruptibly()】
- lockInterruptibly() 方法的实现逻辑与 lock() 大致相同,lockInterruptibly() 遇到线程中断的情况会抛出 InterruptedException 异常,lock() 不会抛出任何异常。
- 同样加入 CHL 队列锁,不同的是 线程中断的时候,以下方法会抛出 InterruptedException 异常。
【解锁操作 unlock()】
- unlock() 方法用于释放锁。如果当前线程是锁的持有者,则将持有锁的数量减 1;若持有锁的数量为 0,则锁已经被释放;如果当前线程不是锁的持有者,调用 unlock() 方法会抛出 IllegalMonitorStateException 异常。
- 使用 tryRelease 释放锁,如果释放锁成功,则返回 true;释放锁失败则返回 false。
- tryRelease 方法中首先判断当前线程是否持有锁(即判断当前线程是否为独占锁的线程),如果不是则抛出 IllegalMonitorStateException 异常。getState() 返回当前线程持有锁的数量,减去 releases (1) 得到释放锁后当前线程持有锁的数量 c。
- 如果 c 为 0,则说明当前线程已经释放了锁,设置 free 为 true,设置独占锁的线程为 null。
- 更新锁的状态为 c,返回是否释放锁 free。
- tryRelease 返回 true,若 head 节点不为空且其 waitStatus(等待状态)不为 0,则调用 unparkSuccessor 唤醒队列的第一个线程节点