一 ReentrantLocak特性(对比synchronized)
(1)尝试获得锁,锁获取超时
(2)获取到锁的线程能够响应中断
ReentrantLock类在java.util.concurrent.locks包中,ReentrantLock实现Lock接口,并且在ReentrantLock中引用了AbstractQueuedSynchronizer的子类。
所有的同步操作都是依靠AbstractQueuedSynchronizer(AQS,队列同步器)的子类实现。AbstractQueuedSynchronizer维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。

AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
自定义同步器实现时主要实现以下几种方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。 tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。 tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。 tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。
再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与子线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
二 源码解析
ReentrantLock支持两种锁模式,公平锁和非公平锁。默认的实现是非公平的,具体见构造方法:
public ReentrantLock() { sync = new NonfairSync();//创建非公平锁 } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();//根据传入值创建公平锁或者非公平锁 }
Sync、FairSync和NonfairSync都是ReentrantLock为了实现自己的需求而实现的内部类,在加锁时,会通过这些内部类来实现。
公平锁的lock加锁方法:
static final class FairSync extends Sync {
//执行加锁操作 final void lock() { acquire(1); }
/**
函数流程如下:
tryAcquire()尝试直接去获取资源,如果成功则直接返回;否则执行acquireQuered方法
如果tryAcquire加锁失败,则调用addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
*/
public final void acquire(int arg) { //首先调用tryAcquire尝试一次加锁,如果加锁成功则执行selfInterrupt,否则调用addWaiter加入到阻塞队列中 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } protected final boolean tryAcquire(int acquires) {//尝试加锁 final Thread current = Thread.currentThread();//获取当前线程 int c = getState();//获取共享变量state if (c == 0) {//如果state==0说明目前还没有线程加锁 // 1. 和非公平锁相比,这里多了一个判断:队列是否为空,或者是否有其他线程在等待,如果没有其他线程等待,则执行CAS操作 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) {//如果state!=0,说明已经有线程获取了锁,因此判断已加锁的线程是否为当前线程,如果是,则state+1,从而实现可重入 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
//公平锁在加锁前需要通过这个方法判断当前队列是否为空,或者当前线程是否为队列的第一个节点,如果为空或者是第一个节点,则尝试加锁,否则加入到队列尾部
public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
这里如果h == t,则表明头结点和尾节点指向同一个节点,因此 h != t 表明队列不为空,如果队列不为空,则判断头结点的下一个节点是否为当前线程。
非公平锁NonfairSync的lock方法如下:
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
从源码中可以看出,主要通过CSA操作来实现加锁,通过调用Unsafe的native方法,判断内存中的值和期望值0是否相等,相等则设置为1表示加锁成功,把当前线程设置为ExclusiveOwnerThread即独占线程,如果CAS操作失败,则加锁失败,执行acquire(int arg)方法,acquire是父类AbstractQueuedSynchronizer的方法,子类FairSync和NonfairSync重写了tryAcquire方法:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
tryAcquire:重新进行一次锁获取和进行锁重入的处理。 addWaiter:将线程添加到等待队列中。 acquireQueued:自旋获取锁。selfInterrupt:中断线程。
三个条件的关系为and,如果 acquireQueued返回true,那么线程被中断selfInterrupt会中断线程。
NonfairSync的tryAcquire调用了父类Sync的nonfairTryAcquire方法,主要是做重入锁的实现,synchronized本身支持锁的重入,而ReentrantLock则是通过此处实现。
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread();//获取当前要加锁的线程 int c = getState(); if (c == 0) {//(1)锁状态为0时表示当前没有线程加锁,重新尝试获取锁 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) {//(2)表示当前已有线程加锁,做一次是否当前线程为占用锁的线程的判断,如果是一样的那么进行计数 int nextc = c + acquires;//记录当前线程重新请求加锁次数 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true;//返回加锁成功 } return false;//返回加锁失败 }
(1)在锁状态为0时表示当前没有线程加锁,重新尝试获取锁。
(2)否则表示当前已有线程加锁,那么做一次是否当前线程为占用锁的线程的判断,如果是一样的那么进行计数,当然在锁的relase过程中会进行递减,保证锁的正常释放。
如果tryAcquire(arg)返回false,表示当前线程加锁失败,那么把线程添加到等待队列中,调用addWaiter加入到队列中。
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode);//根据当前线程创建一个Node // Try the fast path of enq; backup to full enq on failure Node pred = tail;//获取尾节点 if (pred != null) {//说明队列尾节点已经初始化,因此加入到队列尾部 node.prev = pred;//当前节点的前驱设置为尾节点 if (compareAndSetTail(pred, node)) {//通过CAS操作设置尾节点为当前节点,如果设置失败则调用enq通过自旋加入到队列中 pred.next = node;//尾节点设置成功,则前驱节点的next设置为当前节点 return node; } } enq(node); return node; }
private Node enq(final Node node) {//通过自旋将node加入到队列中 for (;;) { Node t = tail; if (t == null) { // 如果尾节点为空,则需要先进行初始化操作,通过CAS操作设置head节点,设置成功则赋值为tail,head和tail可以看成虚节点 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
Node结点是对每一个访问同步代码的线程的封装,其包含了需要同步的线程本身以及线程的状态,如是否被阻塞,是否等待唤醒,是否已经被取消等。变量waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。
CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。 SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。 CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。 PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。 0状态:值为0,代表初始化状态。
AQS在判断状态时,通过用waitStatus>0表示取消状态,而waitStatus<0表示有效状态。
通过tryAcquire()和addWaiter(),该线程获取资源失败,已经被放入等待队列尾部了,加入队列成功后,调用acquireQueued方法通过自旋进行加锁:
final boolean acquireQueued(final Node node, int arg) { boolean failed = true;//标记是否成功拿到资源 try { boolean interrupted = false;//标记等待过程中是否被中断过 //又是一个“自旋”! for (;;) { final Node p = node.predecessor();//拿到前驱 //如果前驱是head,即该结点已经是第一个节点,那么便有资格去尝试加锁(可能是老大释放完资源唤醒自己的,当然也可能被interrupt了)。 if (p == head && tryAcquire(arg)) { setHead(node);//加锁成功后,将head指向该结点。所以head所指的标杆结点,就是当前获取到资源的那个结点或null。 p.next = null; // setHead中node.prev已置为null,此处再将head.next置为null,就是为了方便GC回收以前的head结点。也就意味着之前拿完资源的结点出队了! failed = false; return interrupted;//返回等待过程中是否被中断过 } //如果加锁失败,就进入waiting状态,直到被unpark()
//没有得到锁时:
//shouldParkAfterFailedAcquire方法:返回是否需要阻塞当前线程,如果返回true,表示需要阻塞,则执行parkAndCheckInterrupt方法
//parkAndCheckInterrupt方法:阻塞当前线程,直到被unpark,返回是否被中断
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true;//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true } } finally { if (failed) cancelAcquire(node); } }
/** * 获取锁失败时,检查并更新node的waitStatus。 * 如果线程需要阻塞,返回true。 */ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; //前驱节点的waitStatus是SIGNAL。 if (ws == Node.SIGNAL) /* * SIGNAL状态的节点,释放锁后,会唤醒其后继节点。 * 因此,此线程可以安全的阻塞(前驱节点释放锁时,会唤醒此线程)。 */ return true; //前驱节点对应的线程被取消 if (ws > 0) { do { //跳过此前驱节点,一直往前找,直到找到最近的一个正常状态的节点,并排到他的后面 node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* 此时,需要将前驱节点的状态设置为SIGNAL。即告诉前驱节点,在释放的时候去唤醒他的后继节点。 * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } //当shouldParkAfterFailedAcquire方法返回true,则调用parkAndCheckInterrupt方法阻塞当前线程 private final boolean parkAndCheckInterrupt() { LockSupport.park(this);//调用park()使线程进入waiting状态 return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的 }
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。
park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。(再说一句,如果线程状态转换不熟,可以参考本人写的Thread详解)。需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。
加锁具体流程:
(1)调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
(2)没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
(3)acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
(4)如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
acquireQueued函数的具体流程:
(1)结点进入队尾后,检查状态,找到安全休息点;
(2)调用park()进入waiting状态,等待unpark()或interrupt()唤醒自己;
(3)被唤醒后,看自己是不是有资格能拿到号。如果拿到,head指向当前结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,继续流程1。

非公平锁和公平锁区别就在于,当某线程执行tryAcquire时,非公平锁不关心同步队列,直接尝试获取资源,成功了就获取到资源了,失败了才进入到同步队列,而公平锁首先会检查同步队列是不是有节点,如果没有才尝试获取资源,如果有节点则直接进入同步队列排队。
非公平锁和公平锁的两处不同:
1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
公平锁是严格的以FIFO的方式进行锁的竞争,但是非公平锁是无序的锁竞争,刚释放锁的线程很大程度上能比较快的获取到锁,队列中的线程只能等待,所以非公平锁可能会有“饥饿”的问题。
但是重复的锁获取能减小线程之间的切换,而公平锁则是严格的线程切换,这样对操作系统的影响是比较大的,所以非公平锁的吞吐量是大于公平锁的,这也是为什么JDK将非公平锁作为默认的实现。
如果使用ReentrantLock,可能本身是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但这样,如果线程C在读数据、线程D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。
此时可以使用读写锁ReadWriteLock。ReadWriteLock是一个读写锁接口,ReentrantReadWriteLock是ReadWriteLock接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
锁的释放
线程加锁失败,在阻塞之前,都回去修改其前驱节点的waitStatus=Node.SIGNAL。这是为什么?我们看下锁释放的代码,便一目了然
public final boolean release(int arg) { //修改锁计数器,如果计数器为0,说明锁被释放 if (tryRelease(arg)) { Node h = head; //head节点的waitStatus不等于0,说明head节点的后继节点对应的线程,正在阻塞,等待被唤醒 if (h != null && h.waitStatus != 0) //唤醒后继节点 unparkSuccessor(h); return true; } return false; } private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); //后继节点 Node s = node.next; //如果s被取消,跳过被取消节点 if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) //唤醒后继节点 LockSupport.unpark(s.thread); }
这个函数并不复杂。一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃线程,这里我们也用s来表示吧。此时,再和acquireQueued()联系起来,s被唤醒后,进入if (p == head && tryAcquire(arg))的判断(即使p!=head也没关系,它会再进入shouldParkAfterFailedAcquire()寻找一个安全点。这里既然s已经是等待队列中最前边的那个未放弃线程了,那么通过shouldParkAfterFailedAcquire()的调整,s也必然会跑到head的next结点,下一次自旋p==head就成立啦),然后s把自己设置成head标杆结点,表示自己已经获取到资源了,acquire()也返回了!
release()是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。
三 模板方法模式
1 模板方法模式定义
AQS底层使用了模板方法模式。具体含义是定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。其结构如下:

AbstractClass:抽象类。用来定义算法骨架和原语操作,在这个类里面,还可以提供算法中通用的实现
ConcreteClass:具体实现类。用来实现算法骨架中的某些步骤,完成跟特定子类相关的功能。
/** * 定义模板方法、原语操作等的抽象类 */ public abstract class AbstractClass { /** * 原语操作1,所谓原语操作就是抽象的操作,必须要由子类提供实现 */ public abstract void doOperateOne(); /** * 原语操作2 */ public abstract void doOperateTwo(); /** * 模板方法,定义算法骨架 */ public final void templateMethod() { doPrimitiveOperation1(); doPrimitiveOperation2(); } } /** * 具体实现类,实现原语操作 */ public class ConcreteClass extends AbstractClass { public void doOperateOne() { //具体的实现 } public void doOperateTwo() { //具体的实现 } }
2 AQS 中使用的模板方法模式
在AbstractQueuedSynchronizer中定义的模板方法为:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
可以看出该方法是final型的,即不能被子类重写。子类需要做的是重写tryAcquire方法,即获取资源(设置state变量)的方式,如下可以看到这个方法载AbstractQueuedSynchronizer中是默认抛出异常,即子类要想实现独占锁,这个方法必须复写。
protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); }

同理,AbstractQueuedSynchronizer的release方法也是一个模板方法,子类无法重写,而且必须重写tryRelease方法。
AQS的模板方法总结如下:
(1)独占式获取
acquire()、acquireInterrutpibly()、tryAcquireNanos()
(2)共享式获取
acquireShared()、acquireSharedInterruptibly()、tryAcquireSharedNanos()
(3)独占式释放
release()
(4)共享式释放
releaseShared()
四 基于AQS的自定义锁的实现
自定义锁的实现大体流程:确定实现独占还是共享锁 --> 继承AQS -->实现tryAcquire和tryRelease方法;具体需要实现的方法如下:
(1)独占式获取
tryAcquire()
(2)共享式获取
tryAcquireShared()
(3)独占式释放
tryRelease()
(4)共享式释放
tryReleaseShared()
(5)isHeldExclusively() :该方法返回同步状态,需要自己覆盖实现
public class MyReentrantLock implements Lock { /** * 写成静态内部类,继承AQS模板类 */ private static class Sync extends AbstractQueuedSynchronizer { /** * 尝试获取锁的方法 * 需要自己实现的流程方法 */ @Override protected boolean tryAcquire(int arg) { //CAS比较内存中的原始值为0,则修改为传入的状态值1,当前线程获取到锁 if (compareAndSetState(0, arg)) { setExclusiveOwnerThread(Thread.currentThread());//当前线程得到了锁,则将当前得到锁的线程设置为独占线程 return true; } return false; } /** * 释放锁的方法,需要实现 */ @Override protected boolean tryRelease(int arg) { if (getState() == 0) {//判断状态是否为0,为0则直接抛出不支持操作的异常,增强健壮性的代码 throw new UnsupportedOperationException(); } setExclusiveOwnerThread(null);//将当前独占线程设置为null setState(0);//将当前标志锁状态的值设置为0,表示锁已经释放 return true; } /** * 是否同步独占,true--已被独占,false--未被独占 */ @Override protected boolean isHeldExclusively() { return getState() == 1; } Condition newCondition() { return new ConditionObject();//AQS已经实现Condition,此处只需要直接实例化并使用AQS中的实现即可 } } private Sync sync = new Sync(); @Override public void lock() { sync.acquire(1); //获取锁 } @Override public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1);//获取锁,允许获取过程中有中断 } @Override public boolean tryLock() { return sync.tryAcquire(1); } @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(time));//获取锁,有超时机制 } @Override public void unlock() { sync.release(1);//释放锁 } @Override public Condition newCondition() { return sync.newCondition();//获取AQS中的Condition实例,用于等待、唤醒操作 } }
public class TestMyLock { static MyReentrantLock lock = new MyReentrantLock(); public static void main(String[] args) { for(int i=0;i<5;i++){ Thread threadA = new Thread(() -> { lock.lock(); try{ System.out.println("获取到锁处理业务逻辑"); Thread.sleep(1000) ; } catch (InterruptedException e) { e.printStackTrace(); } finally{ lock.unlock(); System.out.println("释放锁"); } }); threadA.start(); } } }
五 ReentrantLock中方法介绍
1 getHoldCount()
getHoldCount()方法返回的是当前线程调用lock()的次数,源码如下:
public int getHoldCount() { return sync.getHoldCount(); } final int getHoldCount() { return isHeldExclusively() ? getState() : 0; }
ReentrantLock和synchronized一样,锁都是可重入的,同一线程的同一个ReentrantLock的lock()方法被调用了多少次,getHoldCount()方法就返回多少
2 getQueueLength()和isFair()
getQueueLength()方法用于获取正等待获取此锁定的线程估计数。注意"估计"两个字,因为此方法遍历内部数据结构的同时,线程的数据可能动态变化
isFair()用来获取此锁是否公平锁
public final int getQueueLength() { int n = 0; for (Node p = tail; p != null; p = p.prev) { if (p.thread != null) ++n; } return n; }
public final boolean isFair() { return sync instanceof FairSync; }
参考:
1、Java并发之AQS详解 https://www.cnblogs.com/waterystone/p/4920797.html
2、Java并发之线程中断 https://www.cnblogs.com/yangming1996/p/7612653.html
3、ReentrantLock实现机制(CLH队列锁) https://www.jianshu.com/p/b6efbdbdc6fa
https://mp.weixin.qq.com/s/ae60O68UofpuO0Cq5OEnNA
https://mp.weixin.qq.com/s/ae60O68UofpuO0Cq5OEnNA
https://mp.weixin.qq.com/s/GI12aDOaSAdvbhYjkLQ1rA
浙公网安备 33010602011771号