本文共 13874 字,大约阅读时间需要 46 分钟。
AQS队列同步器,java.util.concurrent包中很多类都依赖于这个类所提供的队列式的同步器,比如说常用的ReentranLock,Semaphore和CountDownLatch等
AQS详解可以查看博客:
ReentranLock和Synchronized功能类似,但是Synchronized的阻塞无法被中断,而ReentranLock则提供了可中断的阻塞。ReentranLock最常用的就是如下方法:
ReentrantLock lock = new ReentrantLock();lock.lock();lock.unlock();
ReentranLock分为公平锁和非公平锁。二者的区别就在获取锁是否和排队顺序相关。
如果当前锁被另一个线程持有,那么当前申请锁的线程会被挂起等待,然后加入一个等待队列里。
理论上,先调用lock函数被挂起等待的线程应该排在等待队列的前端,后调用的就排在后边。如果此时,锁被释放,需要通知等待线程再次尝试获取锁,公平锁会让最先进入队列的线程获得锁,而非公平锁则会唤醒所有线程,让它们再次尝试获取锁,所以可能会导致后来的线程先获得了锁,则就是非公平。
ReentranLock的构造函数中可以传入一个boolean变量,确定是否适用公平锁
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();}
FairSync和NonfairSync都继承了Sync类,而Sync的父类就是AbstractQueuedSynchronizer
ReentranLock的lock函数如下所示,直接调用了sync的lock函数,也就是调用了FairSync的lock函数:
//ReentranLockpublic void lock() { sync.lock();}//FairSyncfinal void lock() { acquire(1);//调用了AQS的acquire函数} //NonfairSync final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1);}
acquire函数可以理解为获取一个同一时间只能有一个函数获取的量,这个量就是锁概念的抽象化,acquire源码如下:
public final void acquire(int arg) { //tryAcquire先尝试获取"锁",如果成功,直接返回,失败继续执行后续代码 //addWaiter是给当前线程创建一个节点,并将其加入等待队列 //acquireQueued是当线程已经加入等待队列之后的行为 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }}
tryAcquire、addWaiter、acquireQueued三个函数非常重要,这里分别进行讲述
tryAcquire源码
//AQS类中的变量private volatile int state;//这是FairSync的实现,AQS中未实现,子类按照自己的需要实现该类protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); //获取AQS中的state变量,代表抽象概念的锁 int c = getState(); if (c == 0) { //值为0,那么当前独占性变量还未被线程占有 if (!hasQueuedPredecessors() && //如果当前阻塞队列上没有先来的线程在等待,UnfairSync这里的实现就不一致 compareAndSetState(0, acquires)) { //成功cas,那么代表当前线程获得该变量的所有权,也就是说成功获得锁 setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { //如果该线程已经获取了独占性变量的所有权,那么根据重入性原理,将state值进行加1,表示多次lock //由于已经获得锁,该段代码只会被一个线程同时执行,所以不需要进行任何并行处理 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } //上述情况都不符合,说明获取锁失败,返回false return false;}
由上述代码我们可以发现,tryAcquire就是尝试获取那个线程独占的变量state。
state的值表示其状态:
- 如果是0,那么当前还没有线程独占此变量
- 否则就是已经有线程独占了这个变量,也就是代表已经有线程获得了锁。但是这个时候要再进行一次判断,看是否是当前线程自己获得的这个锁,如果是,那么就增加state的值。
注意:
addWaiter源码
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); //先使用快速入列法来尝试一下,如果失败,则进行更加完备的入列算法 Node pred = tail;//列尾指针 if (pred != null) { node.prev = pred; //步骤1:该节点的前趋指针指向tail if (compareAndSetTail(pred, node)){ //步骤二:cas将尾指针指向该节点 pred.next = node;//步骤三:如果成功,让旧列尾节点的next指针指向该节点 return node; } } //cas失败,或在pred == null时调用enq enq(node); return node;}private Node enq(final Node node) { for (;;) { //cas无锁算法的标准for循环,不停的尝试 Node t = tail; if (t == null) { //初始化 if (compareAndSetHead(new Node())) //需要注意的是head是一个哨兵的作用,并不代表某个要获取锁的线程节点 tail = head; } else { //和addWaiter中一致,不过有了外侧的无限循环,不停的尝试,自旋锁 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } }}
通过调用addWaiter函数,AQS将当前线程加入到了等待队列,但是还没有阻塞当前线程的执行,接下来分析一下acquireQueued函数
acquireQueued源码
由于进入阻塞状态的操作会降低执行效率,所以AQS会尽力避免试图获取独占性变量的线程进入阻塞状态。
所以,当线程加入等待队列之后,acquireQueued会执行一个for循环,每次都判断当前节点是否应该获得这个变量(在队首了)。
如果不应该获取或在再次尝试获取失败,那么就调用shouldParkAfterFailedAcquire判断是否应该进入阻塞状态,如果当前节点之前的节点已经进入阻塞状态了,那么就可以判定当前节点不可能获取到锁,为了防止CPU不停的执行for循环,消耗CPU资源,调用parkAndCheckInterrupt函数来进入阻塞状态。
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { //一直执行,直到获取锁,返回 final Node p = node.predecessor(); //node的前驱如果是head,就说明node是将要获取锁的下一个节点 if (p == head && tryAcquire(arg)) { //所以再次尝试获取独占性变量 setHead(node); //如果成功,那么就将自己设置为head p.next = null; //help GC failed = false; return interrupted;//此时,还没有进入阻塞状态,所以直接返回false,表示不需要中断 } //判断是否要进入阻塞状态 //如果shouldParkAfterFailedAcquire返回true表示需要进入阻塞,调用parkAndCheckInterrupt, //否则表示还可以再次尝试获取锁,继续进行for循环 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //调用parkAndCheckInterrupt进行阻塞,然后返回是否为中断状态 interrupted = true; } } finally { if (failed) cancelAcquire(node); }}private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) //前一个节点在等待独占性变量释放的通知,所以,当前节点可以阻塞 return true; if (ws > 0) { //前一个节点处于取消获取独占性变量的状态,所以,可以跳过去 //返回false do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //将上一个节点的状态设置为signal,返回false, compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false;}private final boolean parkAndCheckInterrupt() { LockSupport.park(this); //将AQS对象自己传入 return Thread.interrupted();}
阻塞和中断
由上述分析可知,AQS通过调用LockSupport的park方法来执行阻塞当前进程的操作。其实,这里的阻塞就是线程不再执行的含义。通过调用这个函数,线程进入阻塞状态,上述的lock操作也就阻塞了,等待中断或独占性变量被释放
public static void park(Object blocker) { Thread t = Thread.currentThread(); setBlocker(t, blocker);//设置阻塞对象,用来记录线程被谁阻塞的,用于线程监控和分析工具来定位 UNSAFE.park(false, 0L);//让当前线程不再被线程调度,就是当前线程不再执行. setBlocker(t, null);}
与lock操作类似,unlock操作调用了AQS的relase方法,参数和调用acquire时一样,都是1
public final boolean release(int arg) { if (tryRelease(arg)) { //释放独占性变量,起始就是将status的值减1,因为acquire时是加1 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h);//唤醒head的后继节点 return true; } return false;}
由上述代码可知,release就是先调用tryRelease来释放独占性变量。如果成功,那么就看一下是否有等待锁的阻塞线程,如果有,就调用unparkSuccessor来唤醒他们
protected final boolean tryRelease(int releases) { //由于只有一个线程可以获得独占先变量,所以,所有操作不需要考虑多线程 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { //如果等于0,那么说明锁应该被释放了,否在表示当前线程有多次lock操作. free = true; setExclusiveOwnerThread(null); } setState(c); return free;}
tryRelease中的逻辑也体现了可重入锁的概念,只有等到state的值为1时,才代表锁真正被释放了。所以独占性变量state的值就代表锁的有无。当state=0时,表示锁未被占有,否则表示当前锁已经被占有
private void unparkSuccessor(Node node) { //一般来说,需要唤醒的线程就是head的下一个节点,但是如果它获取锁的操作被取消,或在节点为null时 //就直接继续往后遍历,找到第一个未取消的后继节点 Node s = node.next; 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方法后,进行lock操作被阻塞的线程就恢复到运行状态,就会再次执行acquireQueued中的无限for循环中的操作,再次尝试获取锁。
同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。可重写的方法有:
同步器的设计是基于模板方法模式, 使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法
对同步状态进行更改,这时就需要使用同步器提供的3个方法来进行操作
来一个自定义AQS的例子:
public class AbstractQueuedSynchronizerDemo implements Lock { private final Sync sync = new Sync(2); // 静态内部类,自定义同步器 private static final class Sync extends AbstractQueuedSynchronizer { Sync(int count) { if (count <= 0) { throw new IllegalArgumentException("count must large than zero."); } setState(count); } public int tryAcquireShared(int reduceCount) { for (;;) { int current = getState(); int newCount = current - reduceCount; if (newCount < 0 || compareAndSetState(current, newCount)) { return newCount; } } } public boolean tryReleaseShared(int returnCount) { for (;;) { int current = getState(); int newCount = current + returnCount; if (compareAndSetState(current, newCount)) { return true; } } } final ConditionObject newCondition() { return new ConditionObject(); } } public void lock() { sync.acquireShared(1); } public void unlock() { sync.releaseShared(1); } public void lockInterruptibly() throws InterruptedException { sync.acquireSharedInterruptibly(1); } public boolean tryLock() { return sync.tryAcquireShared(1) >= 0; } public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sync.tryAcquireSharedNanos(1, unit.toNanos(time)); } @Override public Condition newCondition() { return sync.newCondition(); }}
public class AbstractQueuedSynchronizerMain { public void test() { final Lock lock = new AbstractQueuedSynchronizerDemo(); class Worker extends Thread { public void run() { while (true) { lock.lock(); try { Thread.sleep(1000); System.out.println(Thread.currentThread().getName()); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } // 启动10个子线程 for (int i = 0; i < 10; i++) { Worker w = new Worker(); w.setDaemon(true); w.start(); } // 主线程每隔1秒换行 for (int i = 0; i < 10; i++) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(); } } public static void main(String[] args) { AbstractQueuedSynchronizerMain testMyLock = new AbstractQueuedSynchronizerMain(); testMyLock.test(); }}
LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也成为构建同步组件的基础工具,应用极其广泛。LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程,使用非常方便。
和Object的wait和notify/notifyAll对比
阻塞和唤醒线程的方式,按照以前我们可以使用Object的wait和notify/notifyAll方法实现,如下:
public class TestWait { public static void main(String[] args)throws Exception { final Object obj = new Object(); Thread A = new Thread(new Runnable() { @Override public void run() { int sum = 0; for(int i=0;i<10;i++){ sum+=i; } try { obj.wait(); }catch (Exception e){ e.printStackTrace(); } System.out.println(sum); } }); A.start(); //睡眠一秒,保证线程A已经计算完成,阻塞在wait方法 Thread.sleep(1000); obj.notify(); }}
这一段代码会报错,因为Object的wait和notify/notifyAll方法必须在同步代码块中才能使用!!!因此,我们改进一下:
public class TestWait { public static void main(String[] args)throws Exception { final Object obj = new Object(); Thread A = new Thread(new Runnable() { @Override public void run() { int sum = 0; for(int i=0;i<10;i++){ sum+=i; } try { synchronized (obj){ obj.wait(); } }catch (Exception e){ e.printStackTrace(); } System.out.println(sum); } }); A.start(); //睡眠一秒,保证线程A已经计算完成,阻塞在wait方法 Thread.sleep(1000); synchronized (obj){ obj.notify(); } }}
我们再来看看使用LockSupport是如何简单实现的:
public class TestWait { public static void main(String[] args)throws Exception { Thread A = new Thread(new Runnable() { @Override public void run() { int sum = 0; for(int i=0;i<10;i++){ sum+=i; } LockSupport.park(); System.out.println(sum); } }); A.start(); LockSupport.unpark(A); }}
对比结果:
- LockSupport不需要在同步代码块里,线程间不需要维护一个共享的同步对象,实现线程间的解耦。
- unpark函数可以先于park调用,所以不需要担心线程间的执行的先后顺序
原理分析
LockSupport的park方法内部调用了Unsafe的park方法,是一个本地native方法,只能通过openjdk的源码查看其本地实现。本地实现中维护了一个int类型的变量_counter,所有的park和unpark都在围绕这个变量进行的。
- 调用park时,判断_counter是否大于零,如果大于零则将_counter设置为零,然后退出,不进行等待阻塞(说明之前已经调用过unpark);如果不大于零(说明之前没有调用过unpark),则进行等待阻塞
- 调用unpark时,现将_counter设置为1,然后判断先前的_counter的值是否小于1,如果不小于1,说明没有线程被park,直接退出;如果小于1,则说明之前有线程park,此时需要唤醒
- 多次调用unpark方法和调用一次unpark方法效果一样,因为都是直接将_counter赋值为1,而不是加1
- 线程A连续调用两次LockSupport.unpark(B)方法唤醒线程B,然后线程B连续调用两次LockSupport.park()方法, 线程B依旧会被阻塞。因为两次unpark调用效果跟一次调用一样,只能让线程B的第一次调用park方法不被阻塞,第二次调用依旧会阻塞
同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其加入同步队列。
同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部
通过调用同步器的acquire(int arg)方法可以获取同步状态,其主要逻辑是:
- 首先调用自定义同步器实现的tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部。
- 最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。如果获取不到则阻塞节点中的线程,而被阻塞线程的唤醒主要依靠前驱节点的出队或阻塞线程被中断来实现。
共享式获取与独占式获取最主要的区别在于同一时刻能否有多个线程同时获取到同步状态。
在acquireShared(int arg)方法中,同步器调用tryAcquireShared(int arg)方法尝试获取同步状态,tryAcquireShared(int arg)方法返回值为int类型,当返回值大于等于0时,表示能够获取到同步状态。
因此,在共享式获取的自旋过程中,成功获取到同步状态并退出自旋的条件就是tryAcquireShared(int arg)方法返回值大于等于0
转载地址:http://kzpxi.baihongyu.com/