队列同步器(AQS)源码分析

队列同步器(AQS)简介:

      AbstractQueueSynchronizer,用来构建锁和其他同步组件的基础框架,使用一个int型变量来表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。

       我们可以这么理解,锁是面向使用者的,即我们可以用锁来完成多线程处理的一些问题,而隐藏了实现的细节,而同步器面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理,线程派对,等待与唤醒等底层的操作。相当于使用者使用锁,而AQS来实现锁。

使用场景:比如可重入锁,CountDownLatch等锁和工具类都用到了AQS

使用方式:

   子类继承并实现他的抽象方法来管理同步状态,对同步状态进行修改时,使用同步器提供的3个方法(getState()、setState()、compareAndSetState())来操作,他们能保证状态的改变是安全的。子类推荐为自定义同步组件的静态内部类。

刚才提到的三个方法getState()、setState()、compareAndSetState()都是final方法,我们并不需要去重写,需要重写的方法是下面的这几个方法:


当我们自定义同步组件时,将会调用同步器提供的模板方法,这些模板方法如下:


这些模板方法同样是final的,我们在调用他们时,也会调用到我们之前重写的方法,在后面我们会介绍到这些模板方法。

下面给出一个例子,大概了解一下怎么使用(静态内部类继承AQS)

class Mutex implements Lock {
	// 静态内部类, 自定义同步器
	private static class Sync extends AbstractQueuedSynchronizer {
		// 是否处于占用状态
		protected boolean isHeldExclusively() {
			return getState() == 1;
		}
		// 当状态为0的时候获取锁
		public boolean tryAcquire(int acquires) {
			if (compareAndSetState(0, 1) ) {
				setExclusiveOwnerThread(Thread. currentThread() ) ;
				return true;
			}
			return false;
		}
		// 释放锁, 将状态设置为0
		protected boolean tryRelease(int releases) {
			if (getState() == 0) throw new
					IllegalMonitorStateException() ;
			setExclusiveOwnerThread(null) ;
			setState(0) ;
			return true;
		}
		// 返回一个Condition, 每个condition都包含了一个condition队列
		Condition newCondition() { return new ConditionObject() ; }
	}
	// 仅需要将操作代理到Sync上即可
	private final Sync sync = new Sync() ;
	public void lock() { sync. acquire(1) ; }
	public boolean tryLock() { return sync.tryAcquire(1) ; }
	public void unlock() { sync. release(1) ; }
	public Condition newCondition() { return sync.newCondition() ; }
	public boolean isLocked() { return sync. isHeldExclusively() ; }
	public boolean hasQueuedThreads() { return sync.hasQueuedThreads() ; }
	public void lockInterruptibly() throws InterruptedException {
		sync. acquireInterruptibly(1) ;
	}
	public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
		return sync. tryAcquireNanos(1, unit.toNanos(timeout) ) ;
	}
}

同步队列:

      AQS中很重要的一个数据结构就是同步队列了,他是一个FIFO双向的同步队列。

      当前的线程在获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

    Node节点是AQS中的一个内部类,成员如下

static final class Node {
        /** Marker to indicate a node is waiting in shared mode */
        static final Node SHARED = new Node();
        /** Marker to indicate a node is waiting in exclusive mode */
        static final Node EXCLUSIVE = null;

        /** waitStatus value to indicate thread has cancelled */
        static final int CANCELLED =  1;
        /** waitStatus value to indicate successor's thread needs unparking */
        static final int SIGNAL    = -1;
        /** waitStatus value to indicate thread is waiting on condition */
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should
         * unconditionally propagate
         */
        static final int PROPAGATE = -3;

        volatile int waitStatus;

       
        volatile Node prev;

       
        volatile Node next;

       
        volatile Thread thread;

       
        Node nextWaiter;

        /**
         * Returns true if node is waiting in shared mode.
         */
        final boolean isShared() {
            return nextWaiter == SHARED;
        }

      
        final Node predecessor() throws NullPointerException {
            Node p = prev;
            if (p == null)
                throw new NullPointerException();
            else
                return p;
        }

        Node() {    // Used to establish initial head or SHARED marker
        }

        Node(Thread thread, Node mode) {     // Used by addWaiter
            this.nextWaiter = mode;
            this.thread = thread;
        }

        Node(Thread thread, int waitStatus) { // Used by Condition
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }


而每个同步器中都会持有同步队列的首节点和尾节点

    private transient volatile Node head;
    private transient volatile Node tail;

所以基本结构如下:


刚才说了获取同步状态失败时,就会把线程信息加入一个新构建的节点,然后接入队列的尾部,所以这个加入队列的过程也必须要保证线程的安全,所以同步器有一个基于CAS的设置尾节点的方法:

compareAndSetTail(Node except,Node update)

首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,就会唤醒后续的节点,而后续的节点将会在获取同步状态成功时将自己设置为首节点。设置首节点是由获取同步状态成功的线程来完成的,由于只有一个线程能够成功的获取到同步状态,因此就不需要CAS来保证线程安全了(就只有一个线程)。


同步状态的获取与释放:

       在了解了同步队列的结构后,我们就可以来看看AQS到底是怎样来进行同步状态的获取和释放的。

       获取与释放分为独占式的和共享式的。

      独占式:顾名思义就是同一时刻只能有一个线程获取到锁,其他获取锁线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取到锁。

      共享式:同一时刻能够有多个线程获取到同步状态。

(1)独占式同步状态的获取:

     通过acquire(int arg)方法获取同步状态,该方法对于中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中, 后续对线程进行中断操作时, 线程不会从同步队列中移出。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

这个方法中调用了重写之后tryAcquire(int arg)方法,还调用了addWaiter方法和acquireQueued方法

下面我们一个一个来分析一下这些方法

首先是addWaiter(Node node)方法
private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 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)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

这个方法会使用compareAndSetTail()方法来吧当前线程加入尾节点,如果没有加入成功,就会去调用enq()方法,那我们再看看enq(final Node 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;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

可以看出,enq方法就是一个死循环,而它所完成的工作和addWaiter(Node node)是一样的,都是吧当前的线程加入到同步队列的尾节点处,所以enq方法和addWaiter可以看做是同一个方法来对待,通过CAS设置尾节点的方式,将并发添加节点的请求变得串行化了,也就是保证了尾节点添加是线程安全的。在执行完这两个方法之后,就会去调用acquireQueued方法,我们再看看这个方法

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

同理,这还是一个死循环的方法,它的逻辑是判断前驱节点是不是首结点(这个时候通过之前说的方法当前线程已经成功的加入了同步队列),假如是首节点(只有他的前驱是首节点他才有机会获取同步状态)那就尝试获取同步状态,获取成功的话就把自己设置为首节点,否则就继续循环直到他获取成功为止。

仔细看看这个方法是个无限循环,感觉如果p == head && tryAcquire(arg)条件不满足循环将永远无法结束,当然不会出现死循环,奥秘在于后面的parkAndCheckInterrupt会把当前线程挂起,从而阻塞住线程的调用栈。当然也不是马上把请求不到锁的线程进行阻塞,还要检查该线程的状态,比如如果该线程处于Cancel状态则没有必要,具体的检查在shouldParkAfterFailedAcquire中:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)           
            return true;
        if (ws > 0) {          
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {   
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

检查原则在于:

  • 规则1:如果前继的节点状态为SIGNAL,表明当前节点需要unpark,则返回成功,此时acquireQueued方法的第12行(parkAndCheckInterrupt)将导致线程阻塞
  • 规则2:如果前继节点状态为CANCELLED(ws>0),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法,直至规则1返回true,导致线程阻塞
  • 规则3:如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环,与规则2同

那么我们现在总结一下,介绍了刚才那么多的方法,大多数的方法里面都有一个死循环,这时回到最初的acquire(int arg)方法,我们就大概能够明白这个方法的逻辑了,当获取失败时,那么后面的方法就会把当前线程加入同步队列,并且让该节点一直自旋(相当于被阻塞了),直到他获取到了同步状态成功(别的线程释放了)为止。

那么现在还有一个疑问,假如第一个获取的线程在tryAcquire方法中就获取同步状态成功,直接返回ture,那么线程就不会被包装成节点加入到同步队列中,那队列何来的首节点和尾节点?

其实我们回去看addWaiter和enq方法就会发现这些方法都会去判断是否有尾节点,当发现没有尾节点时,就会在enq方法里就会创建首节点和尾节点(首节点就是尾节点)

(代码片段)

            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } 
说了这么多,对于锁这种并发组件来说的话,从acquire(int arg)这个方法返回就代表当前线程获取了锁,以上就是这个方法的含义。


获取的逻辑图:


(2)独占式同步状态的释放

 刚才讲的是获取,现在讲的是如何释放同步状态。通过调用AQS的release(int arg)方法即可,他会唤醒首节点的后续节点。

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

ps:唤醒的方法unparkSuccessor(Node node)使用了LockSupport工具类


(3)共享式同步状态的获取与释放:

以文件的读写为例,写操作要求对资源的独占式访问,而读操作可以是共享式访问。

一个线程在读时,其他线程也可以读,但是一个线程在写时,其他线程均不能读写。

调用方法acquireShare(int arg)可以共享式获取同步状态,

 public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

当tryAcquireShared返回值大于等于0时,表示可以获取到同步状态,否则进入doAcquireShared(int arg)方法中自旋

private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

同样是一个死循环,不断的进行tryAcquireShare(int arg)方法,直到获取成功,其实它和独占式的差别不大,差别主要在setHeadAndPropagate方法,顾名思义,即在设置head之后多执行了一步propagate操作

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
       
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }

这意味着独占锁某个节点被唤醒之后,它只需要将这个节点设置成head就完事了,而共享锁不一样,某个节点被设置为head之后,如果它的后继节点是SHARED状态的,那么将继续通过doReleaseShared方法尝试往后唤醒节点,实现了共享状态的向后传播

释放同步状态使用releaseShare(int arg)方法

 public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的, 因为释放同步状态的操作会同时来自多个线程。

(4)独占式超时获取同步状态

      先介绍一下响应中断的同步状态获取过程。在Java 5之前,当一个线程获取不到锁而被阻塞在synchronized之外时,对该线程进行中断操作,此时该线程的中断标志位会被修改, 但线程依旧会阻塞在synchronized上,等待着获取锁。在Java 5中,同步器提供了acquireInterruptibly(int arg)方法, 这个方法在等待获取同步状态时, 如果当前线程被中断, 会立刻返回, 并抛出InterruptedException。超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”,doAcquireNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上, 增加了超时获取的特性。 针对超时获取, 主要需要计算出需要睡眠的时间间隔nanosTimeout, 为了防止过早通知,nanosTimeout计算公式为: nanosTimeout-=now-lastTime, 其中now为当前唤醒时间, lastTime为上次唤醒时间, 如果nanosTimeout大于0则表示超时时间未到, 需要继续睡眠nanosTimeout纳秒,反之, 表示已经超时

private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

该方法在自旋过程中,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功则从该方法返回,这个过程和独占式同步获取的过程类似,但是在同步状态获取失败的处理上有所不同。如果当前线程获取同步状态失败,则判断是否超时(nanosTimeout小于等于0表示已经超时),如果没有超时,重新计算超时间隔nanosTimeout,然后使当前线程等待nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos(Object blocker,long nanos)方法返回)。如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行超时等待,而是进入快速的自 旋过程。 原因在于,非常短的超时等待无法做到十分精确, 如果这时再进行超时等待, 相反会让nanosTimeout的超时从整体上表现得反而不精确。因此,在超时非常短的场景下, 同步器会进入无条件的快速自旋。



使用的例子

最后给出一个使用的例子——TwinsLock,这个工具类允许在同一时刻,之多两个线程同时访问。

import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Lock;

public class TwinsLock 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;
				}
			}
		}
	}
	public void lock() {
		sync. acquireShared(1) ;
	}
	public void unlock() {
		sync. releaseShared(1) ;
	}
	// 其他接口方法略
}







阅读更多
个人分类: java并发编程 jvm
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

队列同步器(AQS)源码分析

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭