Java中的AQS一些理解


AQS概述

AbstractQueuedSynchronizer(AQS)抽象队列同步器,是Doug Lea实现的一个用于同步多线程的一个组件。java.util.concurrent包下的一些Lock实现类就是基于AQS实现的,如常见的ReentrantLock、ReentrantReadWriteLock等。

AQS内部实现了一个双向队列的数据结构,用于存储在线程请求锁时,需要阻塞的线程。

锁可分为独占锁和共享锁;

独占锁也叫排他锁,就是锁只能被一个线程所持有,如果一个线程持有了独占锁,那么其他线程请求时,将被阻塞,知道持有锁的线程释放锁;

共享锁可以被多个线程持有,共享锁被持有时,可继续被其他请求该锁的线程加锁,如果有一个线程获取到该数据的独占锁,那么其他线程只能对数据加共享锁,不能加排他锁;获取到排他锁的数据可以读写数据,而获取到共享锁的线程只能读数据。

下面将结合AQS、ReentrantLock、ReentrantReadWriteLock 源码理解AQS具体实现方式。


AQS主要结构

AQS中,队列的实现是通过内部类Node实现的,同时还有队首、队尾等成员变量:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    //队列懒加载的,如果锁第一次被线程请求时哦,不会初始化head和tail,没有队列,只有出现多个线程竞争
    //同一个锁时,才创建队列
    private transient volatile Node head;//队首节点,实际不存任何线程,下一个才是等待队列的第一个元素
    private transient volatile Node tail;//队尾节点
    
    private volatile int state;//当前锁被持有的线程个数
    //队列实现类
    static final class Node {
    	static final Node SHARED = new Node();//锁类型,共享锁
    	static final Node EXCLUSIVE = null;//锁类型,独占式锁
    	static final int CANCELLED =  1;//状态,表示线程被取消
        static final int SIGNAL    = -1;//状态,需要被唤醒的状态
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;
        
        volatile int waitStatus;//状态
        volatile Node prev;//当前节点在队列中的前一个节点
        volatile Node next;//当前节点在队列中的后一个节点
        volatile Thread thread;//当前节点对应的线程
        Node nextWaiter;//SHARED 时表示是共享锁,否则链接到Condition队列中的下一个节点
        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;
        }
    }
    
    //请求共享锁,由子类实现,返回大于0表示加锁成功
	protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }
    //请求独占锁,由子类实现,返回true表示加锁成功
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }
    //释放独占锁,由子类实现,返回true表示当前锁无线程占用,抛出异常表示释放失败
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }
    //释放共享锁,由子类实现,返回true表示当前锁无线程占用,抛出异常表示释放失败
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    ...

需要注意的是,AQS中有四个方法,需要由子类实现,JUC中的一些Lock类就是通过不同的实现方式,实现了可重入锁、共享锁,独占锁等。

具体的这四个方法在不同锁中的实现细节,可以看我另外一篇文章 Java中的锁介绍

AQS中,使用了CAS算法设置变量值,CAS方法可以看成是原子操作,是线程安全的。

独占锁加锁

下面通过ReentrantLock来展开。ReentrantLock是一个可重入锁的独占式锁,可以通过构造方法初始化为公平锁或非公平锁的实现方式,默认是非公平锁。使用方式:

public class LockTest {
    static ReentrantLock lock = new ReentrantLock();
    public void method(){
        lock.lock();
        //doSomething ... 
        lock.unlock();
    }
}

中间的doSomething就是需要同步的代码块,看lock()方法源码,调用的是ReentrantLock内部类Sync的lock()方法,而Sync类继承了AQS,并且有NonfairSync和FairSync两个实现方式,这里我们用非公平锁请求锁过程来展开。

public class ReentrantLock implements Lock, java.io.Serializable {
    private final Sync sync;
    abstract static class Sync extends AbstractQueuedSynchronizer {...}
    public void lock() {
        sync.lock();
    }
    static final class FairSync extends Sync {...}
    static final class NonfairSync extends Sync {
        final void lock() {
        	//CAS算法加锁,原子性操作
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());//加锁成功,设置锁的独占线程为当前线程
            else
                acquire(1);//锁被其他线程占有,请求锁操作
        }
		//获取锁的实现方法
        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
    ...

AQS中,有一个int类型的state成员变量,用于存储锁被线程请求的次数,为0时表示无线程占有该锁。compareAndSetState(CAS)尝试加锁,既设置state为0,设置失败时,说明有其他线程已经持有该锁,此时调用acquire(1)方法,继续请求锁,请求数量为1.

acquire方法是AQS中的方法:

    public final void acquire(int arg) {
    	//如果请求锁失败,并且当前线程成功加入队列中时
        if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();//阻塞当前线程
    }

acquire方法调用的是tryAcquire方法来判断是否成功加锁,该方法在NonfairSync类中,调用了Sync类中的nonfairTryAcquire方法:

        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//当前线程
            int c = getState();//获取持有锁的线程个数
            if (c == 0) {//没有锁持有线程时
                if (compareAndSetState(0, acquires)) {//通过CAS加锁
                    setExclusiveOwnerThread(current);//设置当前线程为锁的独占式持有线程
                    return true;
                }
            }
            //如果c>0,说明有其他锁持有该线程,如果持有锁的线程也是当前线程,那么发生了重入的情况,可以获取锁
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;//获取锁的数量加1
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            //否则获取失败
            return false;
        }

可以看到,ReentrantLock是可重入锁。如果获取锁失败时,将调用acquireQueued方法,并且在调用acquireQueued方法前,先调用了addWaiter方法把当前线程加入队列中,addWaiter方法:

	//参数mode是Node类的SHARED或EXCLUSIVE,表明该线程想获取的是独占式锁还是共享锁
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 这里是快速将节点插入队列的操作
        Node pred = tail;
        if (pred != null) {//队尾不为空,说明队列中有等待的线程
            node.prev = pred;
            // CAS方式插入队列尾部,如果失败,则可能出现有其他线程也请求锁,并已经进入队列中
            // 或者尾部的那个线程已经释放锁,不在队列中了
            if (compareAndSetTail(pred, node)) {
                pred.next = node;//成功插入队列尾部
                return node;
            }
        }
        //如果队尾为空,或者出现插入队列尾部失败时,调用enq方法插入队列
        enq(node);
        return node;//返回新创建的节点
    }

快速插入队尾失败时,可能是队列为空,或者出现其他线程插入了队列尾部,这时调用enq方法中,通过自旋的方式不断尝试将该线程插入队尾中;并且如果队列为空时,将初始化队列:

	//node为需要插入队列的节点
    private Node enq(final Node node) {
        for (;;) {//自旋
            Node t = tail;
            // 如果队尾为空,说明需要初始化队列,head节点new Node()即可,不存储线程
            // 同时继续循环,设置队尾
            if (t == null) {
                if (compareAndSetHead(new Node()))//CAS设置队首
                    tail = head;
            } else {//同addWaiter方法中一样,插入队尾
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

总的来说,addWaiter方法就是将当前线程先快速插入到等待队列的尾部,如果队列为空时,或者插入队列尾部失败,则以自旋的方式继续尝试插入队列,直到成功。

addWaiter方法执行完后,此时当前请求锁的线程已经被插入队列尾部了,并返回当前线程的Node节点,但当前线程还没有阻塞。

在acquire方法中,我们发现这个节点被传入acquireQueued方法了。

acquireQueued方法主要用于阻塞当前线程,并保证在阻塞后被重新唤醒时能够继续去执行请求锁的操作。

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; // 释放上一个队首的在队列中的链接,帮助JVM回收
                    failed = false;
                    return interrupted;//返回线程是否阻塞过
                }
                // 如果获取锁失败,则判断是否在获取锁失败时需要阻塞线程
                // 如果需要阻塞线程,调用parkAndCheckInterrupt方法阻塞线程,线程在这里暂停运行
                // 重新被唤醒时,将继续循环,请求锁
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
        	// 如果线程一次也没有获取到锁,取消该线程
            if (failed)
                cancelAcquire(node);
        }
    }

总的来说,acquireQueued就是将当前线程放在一个循环中,如果当前线程是队列中第一个线程,则请求锁,请求不到时如果需要阻塞,则阻塞当前线程;重新被唤醒时继续在循环中请求锁。

shouldParkAfterFailedAcquire方法用于判断在请求锁失败的情况下,是否可以阻塞当前线程,当前面一个节点的状态为SIGNAL时,才表明自己有机会被唤醒,这时才能阻塞当前线程:

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //如果前一个线程的状态是SIGNAL,那么说明当前线程有机会被重新唤醒,此时可以阻塞
        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;
    }

至此,独占锁的加锁过程已经完成。

总的来说,独占锁的加锁过程如下图所示

在这里插入图片描述

独占锁释放

独占锁的释放,调用了AQS中的release方法:

    public void unlock() {
        sync.release(1);
    }
    public final boolean release(int arg) {
    	//释放锁
        if (tryRelease(arg)) {
            Node h = head;
            //成功释放锁后,需要唤醒下一个线程
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;//成功释放
        }
        return false;//释放失败
    }

tryRelease方法在Sync类中实现,公平和非公平锁都是调用同一个释放锁的方法:

        protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//释放锁后,持有该锁的线程数
            //如果当前线程没有持有该锁,抛出异常,释放失败
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;//是否有线程持有锁
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);//独占锁释放时,只会是当前线程释放锁,不需要CAS,直接释放即可
            return free;
        }

释放锁仅需将state设置为释放后的state即可。

再看unparkSuccessor方法,该方法是用于唤醒队列中等待锁资源的线程里,位于队伍最前面的线程,唤醒后的线程将继续执行acquireQueued中的代码尝试获取锁:

	// 唤醒node节点后面的线程
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        // 如果node节点状态不是取消状态,将状态设为0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
            
        Node s = node.next;//下一个节点
        // 如果下一个节点为空,或者是取消状态
        // 此时尝试从队列尾部往前,找离node节点最近的一个等待节点
        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);//唤醒线程
    }

共享锁加锁

ReentrantReadWriteLock类也叫读写锁,其内部有两把锁:读锁和写锁,读锁是共享锁,写锁是排他锁,分别由内部类ReadLock和WriteLock实现,同时还有一个内部类Sync,继承了AQS:

public class ReentrantReadWriteLock
        implements ReadWriteLock, java.io.Serializable {
    /** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** Performs all synchronization mechanics */
    final Sync sync;

WriteLock是独占锁,所以调用的是acquire请求加独占锁

    public static class WriteLock implements Lock, java.io.Serializable {
        private final Sync sync;
        protected WriteLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
        public void lock() {
            sync.acquire(1);//请求加独占锁
        }

而ReadLock请求加的是共享锁,调用的是AQS中的acquireShared方法:

    public static class ReadLock implements Lock, java.io.Serializable {
        private final Sync sync;
        protected ReadLock(ReentrantReadWriteLock lock) {
            sync = lock.sync;
        }
        public void lock() {
            sync.acquireShared(1);//请求加共享锁
        }
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

acquireShared方法调用tryAcquireShared方法请求获取共享锁,该方法由子类实现。

如果返回值小于0,则表明获取共享锁失败,此时将调用doAcquireShared方法;如果等于0,则表示获取当前共享锁成功,并且共享锁数量已经用完,后续线程不能再获取锁;如果大于0,则表示当前线程获取锁成功,并且还有剩余的锁资源可被其他线程获取。

doAcquireShared和acquireQueued方法类似,也是将当前线程加入队列,并判断是否需要阻塞当前线程:

    private void doAcquireShared(int arg) {
    	//这里的mode是SHARED,表明加入队列的是请求共享锁类型的线程
        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);
        }
    }

不同的地方在于当请求到锁时,独占锁调用了setHead方法,而共享锁调用了setHeadAndPropagate方法:

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }
	// 这里的propagate参数一定大于等于0
	// 等于0时表示本次获取锁成功后,共享锁资源已经被用完
	// 大于0说明还有锁资源可被其他线程使用
    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();
        }
    }

如果propagate大于0,说明本次请求完后,还有共享锁资源可使用,此时可尝试去唤醒队列等待中线程去请求锁,唤醒操作在doReleaseShared方法中。

如果不满足propagate大于0,即等于0,无共享锁资源可使用,需要注意的是,由于当前是多线程环境,所以propagate等于0,并不代表无共享锁资源可用,也可能出现在当前线程运行到if判断时,有其他线程正好释放了锁。

这里,h是旧表头的引用,h == null的判断仅为了避免空指针错误,因为在此之前已经调用了addWaiter方法,所以不会出现h==null的情况,而 (h = head) ==null时,h引用已经变成新队首节点的引用了。

如果h.waitStatus < 0,此时就可能出现其他线程在当前线程运行到这里时,释放了锁,至于为什么,需要看doReleaseShared中的逻辑,这个方法是释放共享锁时的逻辑。

    private void doReleaseShared() {
        for (;;) {
            Node h = head;
            // 如果队列中有等待的线程
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                	//将队首状态设为0
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue; // loop to recheck cases
                    unparkSuccessor(h);// 锁成功释放,并唤醒等待线程
                }
                // 如果ws ==0 则说明有其他线程在执行上一个if操作
                // 此时将状态设为PROPAGATE,以便被其他线程检测到
                else if (ws == 0 &&  !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue; // loop on failed CAS
            }
            if (h == head) // loop if head changed
                break;
        }
    }

可以看到,在doReleaseShared方法中,有一步是将head的状态设置为PROPAGATE的,这个值也是负数,说明有其他线程在释放锁。

再回到setHeadAndPropagate方法中的if判断,第一个h.waitStatus < 0表明有其他线程在释放锁;第二个h.waitStatus < 0,此时的h已经是新队首元素,此时h.waitStatus < 0小于0很正常,因为h可能就是刚刚的node,waitStatus值是SIGNAL。

if (s == null || s.isShared()) 表明,如果队列中下一个线程即不是空,并且是独占模式,那不需要唤醒线程,否则再执行doReleaseShared方法,判断是否需要唤醒线程

setHeadAndPropagate中的if判断这样写也是为了减少唤醒不必要的线程,避免唤醒后的线程获取不到锁时再次阻塞

总的来说,共享锁在获取锁时,如果当前线程获取锁后,还有锁资源可被其他线程获取,那么将尝试唤醒其他线程。

共享锁释放

释放锁的代码也很简单,就是调用子类实现的tryReleaseShared方法,如果成功释放,则调用doReleaseShared方法去处理后续的唤醒线程操作

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

本文是本人的学习备忘和知识积累,不足或理解错误之处请包涵和指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值