Java并发之AQS结构及原理分析

Java中的CAS

CAS:全称为Compare And Swap,比较并交换,CAS 是现代操作系统,解决并发问题的一个重要手段。一个CAS涉及到了下面的操作:
假设内存中的原数据V,旧的预期值A,需要修改的新值B
1、比较 A 与 V 是否相等(比较)
2、如果比较相等,将 B 写入 V(交换)
3、返回操作是否成功
当多个线程对资源进行CAS操作时,只有一个线程能够成功,但是不会阻塞其他线程,其他线程收到的是操作失败信号。

以AtomicInteger为例,打开其源码可以发现,Java的CAS操作是通过sun包下Unsafe类实现的,Unsafe类中的方法都是native方法,由JVM本地实现。

AQS实现原理及源码分析

1、概括

AQS,也即是AbstractQueuedSynchronizer,队列同步器,AbstractQueuedSynchronizer类是一个juc包下的抽象类,以继承的方式使用,为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁和相关同步器(信号量、事件,等等)提供一个框架。此类的设计目标是成为依靠单个原子 int 值来表示状态的大多数同步器的一个有用基础。

根据源码中对AbstractQueuedSynchronizer类的介绍,我们可以了解到以下信息:
①、AQS其实就是一个可以给我们实现锁的框架
②、内部实现的关键是:先进先出的队列、state状态
③、定义了内部类ConditionObject
④、拥有两种线程模式:独占模式和共享模式
⑤、在LOCK包中的相关锁(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS来构建
⑥、一般称AQS为同步器

查看AbstractQueuedSynchronizor类的源码,发现AQS最主要的变量有以下三个:

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

private volatile int state; //表示当前有多少线程获取了锁,此变量是共享的,对于互斥锁来说state<=1

AQS实现的内部依赖是一个FIFO的双端队列,当一个线程获取同步状态失败后,AQS会将此线程及其等待状态等信息构成一个Node加入到同步队列尾部,并且阻塞此线程,当其他线程同步状态释放时,会唤醒同步队列的头节点,AQS的结构示意图如下:
在这里插入图片描述
AQS内部通过

protected final int getState() {
   return state;
}
protected final void setState(int newState) {
    state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
   return STATE.compareAndSet(this, expect, update);
} //是原子性的修改,采用CAS实现

上面的方法修改state。

2、AQS源码分析
AQS内部Node类部分常量基变量
	/** 表示节点处于共享模式 */
    static final Node SHARED = new Node();
    /** 标记节点处于独占模式 */
    static final Node EXCLUSIVE = null;

    /** waitStatus的值之一,表示线程被取消了 */
    static final int CANCELLED =  1;
    /** waitStatus的值之一,表示后继线程需要唤醒 */
    static final int SIGNAL    = -1;
    /** waitStatus的值之一,表示线程正等待某一等待条件 */
    static final int CONDITION = -2;
    /** waitStatus的值之一,表示下个acquireShared方法应该无条件地???*/
    static final int PROPAGATE = -3;

    /**
     * 节点的等待状态,以下值的一种:
     *   SIGNAL:     节点的继任节点是(或者将要成为)BLOCKED状态(例如通过LockSupport.park()操作),因此一个节点一旦被释放(解锁)或者取消就需要唤醒(LockSupport.unpack())它的继任节点。
     *   CANCELLED:  节点操作因为超时或者对应的线程被interrupt。节点不应该留在此状态,一旦达到此状态将从CHL队列中踢出。
     *   CONDITION:  表明节点对应的线程因为不满足一个条件(Condition)而被阻塞。
     *   PROPAGATE:  一个releaseShared 应该扩散到其他节点。设置在doReleaseShared方法(只是头结点)中为了确保扩散继续,即使其他操作已经开始执行。
     *   0:          正常状态
     *
     * 非负值意味着节点不需要被唤醒。
     *
     * 这个值被初始化为0,可以使用CAS修改它。
     */
    volatile int waitStatus;
从ReentrantLock的源码入手来深入理解下AQS的实现

AQS一般是以继承的方式使用的,同步组件内部组合一个继承了AQS的子类:Sync
在这里插入图片描述
Sync类是一个抽象类,其实现类有两个:FairSync和NonfairSync,分别对应着公平锁和非公平锁。

ReentrantLock类提供了下面的构造函数来确定使用公平锁或是非公平锁:

public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
获取锁的过程

NonfairSync的lock()实现如下:

final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
     }

lock()方法首先通过CAS尝试将AQS的state从0修改为1(compareAndSetState()方法上面提到过),如果成功,则将占用锁的线程设置为当前线程。如果CAS操作未成功,说明state不等于0,继续执行acquire(1),这个操作由AQS提供。
acquire()具体实现如下:

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

tryAcquire方法尝试获取锁,如果成功就返回,如果不成功,则把当前线程和等待状态信息构适成一个Node节点,并将结点放入同步队列的尾部。然后为同步队列中的当前节点循环等待获取锁,直到成功。

tryAcquire(arg)在NonfairSync中的实现:

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

可以看到,首先是获取state,然后下面的if操作和lock()方法一样,是为了以最简单的方式获得锁。

如果state不为0,再看当前线程是不是锁的owner,如果是owner, 则尝试将状态值增加acquires,如果这个状态值越界,抛出异常;如果没有越界,则设置后返回true。在一定程度上解释了非公平和重入。

如果状态不为0,且当前线程不是owner,则返回false。
回到acquire()方法,tryAcquire返回false,接着执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),先看下addWaiter(Node.EXCLUSIVE),这个方法创建结点并入队,addWaiter()源码如下:

private Node addWaiter(Node mode) {
        Node node = new Node(mode);

        for (;;) {
            Node oldTail = tail;
            if (oldTail != null) {
                node.setPrevRelaxed(oldTail);
                if (compareAndSetTail(oldTail, node)) {
                    oldTail.next = node;
                    return node;
                }
            } else {
                initializeSyncQueue();
            }
        }
    }

addWaiter返回了插入的节点,作为acquireQueued方法的入参

final boolean acquireQueued(final Node node, int arg) {
        boolean interrupted = false;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node))
                    interrupted |= parkAndCheckInterrupt();
            }
        } catch (Throwable t) {
            cancelAcquire(node);
            if (interrupted)
                selfInterrupt();
            throw t;
        }
    }

node.predecessor()返回的是node的前置节点,也就是addWaiter方法返回的节点的前置节点,在这里是head节点,所以p==head成立,进而进行tryAcquire操作,即争用锁, 如果获取成功,则进入if方法体,看下接下来的操作:

  1. 将node设置为头节点。
  2. 将node的前置节点设置的next设置为null。

从上面的分析可以看出,只有队列的第二个节点可以有机会争用锁,如果成功获取锁,则此节点晋升为头节点。对于第三个及以后的节点,if (p == head)条件不成立,首先进行shouldParkAfterFailedAcquire(p, node)操作(争用锁失败的第二个节点也如此), 来看下源码:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 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.
             */
            pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
        }
        return false;
    }

shouldParkAfterFailedAcquire方法是判断一个争用锁的线程是否应该被阻塞。它首先判断一个节点的前置节点的状态是否为Node.SIGNAL,如果是,锁释放的时候,应当通知它,所以它可以安全的阻塞了,返回true。

如果前节点的状态大于0,即为CANCELLED状态时,则会从前节点开始逐步循环找到一个没有被“CANCELLED”节点设置为当前节点的前节点,返回false。在下次循环执行shouldParkAfterFailedAcquire时,返回true。这个操作实际是把队列中CANCELLED的节点剔除掉。

如果shouldParkAfterFailedAcquire返回了true,则会执行:“parkAndCheckInterrupt()”方法,它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作。

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
释放锁的过程

通过ReentrantLock的unlock方法来看下AQS的锁释放过程:

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;
    }

unlock调用AQS的release()来完成, AQS的tryRelease方法由具体子类实现。tryRelease返回true,则会将head传入到unparkSuccessor(Node)方法中并返回true,否则返回false。首先来看看Sync中tryRelease(int)方法实现,如下所示:

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);
            return free;
        }

这个动作可以认为就是一个设置锁状态的操作,而且是将状态减掉传入的参数值(参数是1),如果结果状态为0,就将排它锁的Owner设置为null,以使得其它的线程有机会进行执行。
在排它锁中,加锁的时候状态会增加1(当然可以自己修改这个值),在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加为2、3、4这些值,只有unlock()的次数与lock()的次数对应才会将Owner线程设置为空,而且也只有这种情况下才会返回true。

法unparkSuccessor(Node)中,就意味着真正要释放锁了,它传入的是head节点(head节点是占用锁的节点),看下源码:

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)
            node.compareAndSetWaitStatus(ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node p = tail; p != node && p != null; p = p.prev)
                if (p.waitStatus <= 0)
                    s = p;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

内部首先会发生的动作是获取head节点的next节点,如果获取到的节点不为空,则直接通过:“LockSupport.unpark()”方法来释放对应的被挂起的线程,这样一来将会有一个节点唤醒后继续进入循环进一步尝试tryAcquire()方法来获取锁。

推荐阅读:
浅谈Java并发编程系列(九)—— AQS结构及原理分析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值