J·U·C Java并发包
JUC 是java.util.concurrent的简称,在jdk 1.5时被引入,JUC包的主要作者是并发大师Doug Lea,包含了非常多的并发编程工具类,其中有一个叫做AbstractQueuedSynchronizerd的抽象类,这个类通常被称为AQS,这是一个非常重要的类,可以说是整个JUC并发包的根基之一,它抽象了与线程同步和协作相关的最核心逻辑,很多并发工具类都是基于AQS来实现的,比如最常用的ReentrantLock、CountDownLatch、Semaphore等等。
AQS的核心是两个双向链表队列(同步队列和等待队列)和一个同步状态,同步队列用来记录获取同步状态失败的线程,等待队列用来获取在Condition接口上调用await()方法的线程,同步状态是一个int类型的变量,在不同的实现中有不同的含义。下面以ReentrantLock的独占锁为例来聊聊AQS的原理,关于共享模型、可中断模式、超时模式的实现原理,实际上本质区别不大,限于篇幅,后面再说。
ASQ在ReentrantLock中的应用
ReentrantLock是一种显示锁,作用与synchronized一样,用来做资源的同步,保证共享资源的线程安全,使用范式如下:
Lock lock = new ReentrantLock();lock.lock();try { do something}finally { lock.unlock();}
ReentrantLock有两种模式,一种是公平模式,一种是非公平模式,为了不引入其他的逻辑,这里以默认的非公平锁为例来看看lock()和unlock()的原理。
先来看看RenntrantLock与AQS之间的关系:
public class ReentrantLock implements Lock, java.io.Serializable { abstract static class Sync extends AbstractQueuedSynchronizer { abstract void lock(); protected final boolean isHeldExclusively() { .. } final ConditionObject newCondition() { return new ConditionObject(); } protected final boolean tryRelease(int releases) { .. } final boolean nonfairTryAcquire(int acquires) { ... } } static final class NonfairSync extends Sync { final void lock() { ... } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } } static final class FairSync extends Sync { final void lock() { acquire(1); } protected final boolean tryAcquire(int acquires) { .. } }}
可以看到,Sync继承自AQS,利用Sync实现了公平和非公平两种同步锁,同步队列中的结构如下:
+------+ prev +-----+ +-----+ head | |
加锁过程
下面聚焦到lock这个方法上。
ReentrantLockpublic void lock() { sync.lock(); } NonfairSync final void lock() /** 这里一进来就以cas的方式抢占同步状态 如果当前同步状态值为0,并且成功将其设置为1 则当前线程成功获取锁,lock方法返回。 这里也体现了锁的非公平性: 公平锁:严格按照锁的获取顺序来分配锁 非公平锁:不管后面有没有线程在等待锁资源,都会抢占同步状态 */ if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else /** 如何失败,则调用父类AQS的acquire方法 */ acquire(1); }
下面来看看AQS的acquire()方法:
public final void acquire(int arg) { /** 这里包含了AQS的完成逻辑 1. 尝试调用子类的tryAcquire()方法来获取同步状态值 (这里的获取同步状态就是将同步状态从0设置成1) 2. 如果设置失败,调用addWaiter方法,将当前节点构造成一个Node 节点,然后用cas的方式线程安全的添加到同步队列的末尾 3. 最后调用acquireQueued方法让当前线程进入自旋状态,获取同步状态 失败的话,利用LockSupport.park()方法将当前线程挂起,等待 前驱节点线程唤醒 */ if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
我们一个方法一个方法的看,首先是子类(NonfairSync)的tryAcquire()方法:
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires);}final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //如果当前状态为0,说明锁没有被其他线程获取 if (c == 0) { //尝试设置同步状态,获取锁 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } /** 这里处理锁d 可重入性。 可重入性指的是同一个线程多次调用同一个需要锁的方法时, 不应该被阻塞。 如果上次获取锁的线程是当前线程,就将同步状态+1 */ else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); //这里没有用cas,是因为,如果逻辑走到这里 //当面当前线程已经获取到了锁 setState(nextc); return true; } return false;}
这个方法主要是利用AQS实现了上层组件(ReentrantLock)的锁的语义。
下面再看addWaiter这个方法。
/** 走到这里,说明当先线程获取同步锁失败了*/private Node addWaiter(Node mode) { //创建一个同步队列中的Node节点 Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure /** 这里来了一个 fast try 去将当前这个node节点加入到同步队列的末尾 之所以这么做,是为了尽量避免进入下面enq的自旋, 如果这里设置失败了,再进入自旋状态,从这里也可以看出 Dong Lea的功力,对性能的极致要求 */ Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //前面尝试失败了,就进入自旋cas模式 enq(node); return node;}private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; //这里就是尝试将tail设置成新增的节点 if (compareAndSetTail(t, node)) { t.next = node; return t; } } }}
最后,再来看看acquireQueued这个方法:
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; /** 成功添加到同步队列以后,就进入了自选状态 */ for (;;) { /** 这里仍然不放弃,判断其前驱节点是否为头节点& 能够获取同步状态,如果true,则说明获取到锁 直接返回,跳出自旋 */ final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } /** 如果失败了,这里会做两件事 1、将其前驱节点状态设置为SINGAL,目的是让 前驱节点执行完成时,去通知当前节点唤醒 2、利用LockSupport.park()挂起当前线程 */ if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); }}
到这里,ReentrantLock 独占非公平锁的加锁的加锁过程就完成了,同步队列的状态如下图所示:
总结一下:
尝试获取同步状态;
如果成功,直接返回;
如果失败,构造一个Node节点,CAS+自旋将其线程安全的加入同步队列末尾;
然后每个节点代表的线程进入自旋,设置前驱节点状态为SINGAl并挂起,等待被前驱节点唤醒;
解锁过程
相比于加锁,解锁过程要简单的多,主要就是释放同步状态,下面看看unlock的逻辑:
ReentrantLockpublic void unlock() { sync.release(1); } AQSpublic final boolean release(int arg) { //这里调用子类实现,释放同步状态 if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) //唤醒后面的节点 unparkSuccessor(h); return true; } return false; } ReentrantLock中非公平的实现: protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //这里处理重入锁的多次加锁的问题 //只有当同步状态-为0时,才代表释放锁状态成功 if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
到此,基于AQS的锁的获取和释放就说完了。
关注我的简书号,分享更多的技术学习文章,如果对于学习编程有很多疑惑,没有思路,不知道如何有效率的学习,可以添加我的java
交流学习群:630473711。在群里直接问我,我就是群主,需要最新系统的学习教程也可以管我要。做了很多年开发,对于学习方式,如何提高自己的技术有一定的经验,术业有专攻,多跟有经验的人交流学习,对这个行业信息了解的多,职业发展的空间就越大。