AQS独占锁原理分析

1,156 阅读10分钟

一、前言

        在并发场景下,多个线程同时对某个变量进行写入,可能导致某个线程在写入变量前读取到的是过期的值(不是其他线程已经写入的数据),为了解决此类数据安全问题,java通过锁来控制多个线程访问同一变量的顺序,从而解决该问题。锁有多种分类,比如悲观锁/乐观锁,共享锁/独占锁,公平锁/非公平锁等。

1. 悲观锁/乐观锁

        悲观锁:假设最坏的情况,认为每次拿数据时都会被其他线程修改,所以在拿数据时会先获取锁,其他线程因为获取不到锁一直阻塞到获取锁的线程释放锁。synchronized就是一种悲观锁。  

        乐观锁:总是假设最乐观的情况,认为每次拿数据是其他线程不会修改该数据,因此不用假设,只有在更新数据时需要判断其他线程有没有在这期间修改过该数据,可以通过数据的版本号来实现。java中乐观锁的实现方式是CAS,也是AQS(AbstractQueuedSynchronizer)的基础.

2. 共享锁/独占锁

       共享锁:锁可以被多个线程同时持有,Semaphore、CountdownLatch、ReentrantReadWriteLock中的readerLock都是共享锁。

       独占锁:锁同时只能被一个线程持有,比如ReentrantLock、ReentrantReadWriteLock中的writerLock。

3. 公平锁/非公平锁

        公平/非公平是针对于当多个线程同时竞争一把锁时,应该按照那种机制给线程派锁。公平锁是按照线程申请锁的顺序来获取锁。对于非公平锁来说,获取锁的顺序与申请锁的顺序无关,可能导致后申请锁的线程比先申请锁的线程早获取锁。

二、AQS简介

        AQS是java众多锁的基础,ReentrantLock、Semaphore、CountdownLatch、ReentrantReadWriteLock的实现都依赖AQS。AQS是一种乐观锁,底层大量采用了CAS操作,当多个线程竞争锁时,通过自旋的方式重试。

        AQS内部支持共享锁、独占锁,如果要使用AQS,子类需要分别实现不同的方法。


        如上表所示,AQS对于这些方法的默认实现是抛出UnsupportedOperationException,子类需要覆写。

        AQS的内部核心是通过FIFO(先入先出队列)实现线程获取锁时的排队工作,全局共享的状态变量(state)标识锁的持有情况,CAS+自旋实现乐观锁。

     AQS中的队列是一个双向链表,元素是其内部类Node:

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;

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

         Node中的waitStatus标识当前节点所处的状态,一共有4中状态:  

  • CANCELLED:当线程由于等待超时或者中断,取消竞争锁时,节点的状态置为该值;
  • SIGNAL:与线程的挂起/线程阻塞有关。如果一个节点竞争锁失败时需要挂起,为了确保当锁资源被释放时其能被正常唤醒,要确保其pre节点的waitStatus为SIGNAL;
  • CONDITION:标识当前节点处于条件队列下;
  • PROPAGATE:用于确保释放锁操作操作的传递。

         AQS中通过两个成员变量head、tail管理等待队列,其中head是dummy节点,内部的thread为null,这样做的原因是为了确保除了head节点之外,队列中的节点都能以相同的方式处理。

    /**
    头结点,实际上该节点是dummy节点,其内部的thread为null
     */
    private transient volatile Node head;

    /**
     尾结点
     */
    private transient volatile Node tail;
   /** * 同步状态,共享锁下表示持有锁的线程数量,独占锁下表示重入锁的重入次数. */
   private volatile int state;

下图是AQS独占锁中队列的示意图,每个节点的nextWaiter为null。


三、锁的获取操作

        以ReentrantLock的使用为例,通过调用lock()获取锁,lock()实际上会调用AQS的acquire(),下面我们以acquire()作为入口,分析独占锁的获取过程。

AQS:

public final void acquire(int arg) {  
  //tryAcquire()由子类实现,尝试获取独占锁。查询state是否允许在独占锁模式下获取,如果允许则表示能获取锁
  //addWaiter(),在给定模式(独占锁)下,为当前线程创建节点,并入队列
  //acquireQueued(),对于已经在队列中的线程,在独占锁模式、不中断的条件下获取锁。
  if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))        
     selfInterrupt();
}

ReentrantLock,公平锁tryAcquire实现:

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //c=0表示,当前没有其他线程持有锁
            if (c == 0) {
                //是否有其他线程比当前线程提前尝试获取锁(公平锁的体现)
                //cas将state->1
                //设置独占线程
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            } //如果当前线程已经是独占线程,支持重入。
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

AQS.addWaiter(Node mode):

private Node addWaiter(Node mode) {
        //创建代表当前线程的节点,并设置模式(独占/共享)
        Node node = new Node(Thread.currentThread(), mode);
        // 如果尾结点已经存在,则直接在尾结点后拼接当前节点并返回
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //如果尾结点为null,或者compareAndSetTail执行失败(当前线程修改尾结点的同时,其他线程修改了尾结点,导致cas失败)
        enq(node);
        return node;
    }

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { 
                //如果tail为null,说明head也为null,初始化head,tail。
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                //如果此时tail不为null,在tail后插入当前节点
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

        经过AQS.addWaiter(),线程已经进入FIFO队列中排队,下一步是调用acquireQueued()尝试获取锁。

AQS.acquireQueued(final Node node, int arg):

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                //获取当前节点的前置节点
                final Node p = node.predecessor();
                //只有前置节点是head,当前节点才尝试获取锁。
                if (p == head && tryAcquire(arg)) {
                    //获取到锁,重新设置head.
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //判断当前线程在获取锁失败后是否能挂起,挂起后检查中断状态(调用Thread.interrupted())
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            //在reentranLock.lock()中,cancelAcquire不会得到执行,线程不会因为竞争锁失败而取消获取,而是一直循环/挂起。
            if (failed)
                cancelAcquire(node);
        }
    }

        通过以上代码,我们发现对于acquireQueued()来说,线程会一直尝试获取锁,不会因为中断而退出锁的竞争。对于ReentrantLock,要想能通过中断控制线程放弃获取锁,可以选择lockInterruptibly(),该方法可以通过响应中断的方式放弃对锁的竞争。

AQS.shouldParkAfterFailedAcquire(Node pred, Node node):该方法主要用于在线程竞争锁失败后,判断当前线程是否应该挂起。在文章中对线程waitStatus的描述中,一个线程在竞争锁失败后,如果需要挂起,首先要确保前置节点的waitStatus为SIGNAL,这样前置节点在获取锁成功后,才知道需要唤起后继节点。

//返回值为true表示当前线程能挂起,否则不能挂起。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        //如果waitStatus是SIGNAL,说明线程可以安全挂起。
        if (ws == Node.SIGNAL)
            return true;
        //如果前置节点的waitStatus>0,说明前置节点已经取消锁的竞争,应该从队列中移除前置节点中已经取消锁竞争的节点。
        if (ws > 0) {
            //通过循环向前遍历队列,直到节点的waitStatus<0。
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             *如果waitStatus=0或者PROPAGATE,应该将前置节点的waitStatus置为SIGNAL,下次调用该方法时才能选择挂起当前线程。
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

AQS.parkAndCheckInterrupt():挂起当前线程,等待被唤醒。

private final boolean parkAndCheckInterrupt() {
        //线程挂起,不再往下继续执行;只有当线程被唤醒或者调用了Thread.interrupt(),才继续重复上述获取锁的操作。
        LockSupport.park(this);
        return Thread.interrupted();
    }

当竞争锁失败后,可以通过响应中断的方式退出锁的竞争。

AQS.cancelAcquire(Node node):

//通过响应中断的方式,当前线程退出锁的获取。
private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;
        node.thread = null;

        //跳过前置节点中已经取消锁竞争的节点。
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        //设置节点的waitStatus为CANCELLED
        node.waitStatus = Node.CANCELLED;

        // 如果当前节点是tail,通过CAS将tail设置为pred,这一步可能失败,但是失败了也无挂紧要,因为节点的waitStatus已经
        //被置为CANCELLED,其他线程也可以将当前节点从队列中剥离。
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            //由于当前节点要从等待队列中剥离,需要节点其后继节点的唤醒问题,要么被当前线程唤醒,要么没前置节点所在的线程唤醒。
            int ws;
            //如果pred不是head,而且pred.thread不为null,而且pred.waitStatus=SIGNAL,或者pred.status<0(为PROPAGATE)并将pred.waitStatus置为SIGNAL
            //说明后继节点的唤醒可以由前置节点所在的线程解决,否则只能由当前线程唤醒。
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next); 
            } else {
                //唤醒当前节点的后继节点。
                unparkSuccessor(node);
            }
            node.next = node; // help GC
        }
    }

AQS.unparkSuccessor:唤醒

private void unparkSuccessor(Node node) {
        /*
         * 清除节点的waitStatus,因为当前节点已经离队。
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        //一般来说唤醒的是node的next节点,但是如果next节点是null或者next节点已经取消,则需要找到离node最近的非CANCELLED的节点
        //采用的方式是从尾部开始遍历,知道找到离node最近的非CANCELLED节点。
        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);
    }


四、锁的释放操作

        以ReentranLock为例,线程获取锁成功后,完成相关业务操作后,需要调用unlock()释放锁,unlock()通过调用AQS.release()完成锁的释放,释放锁后,需要唤醒队列中排在首位而且waitStatus != CANCELLED的节点。

//释放锁
public final boolean release(int arg) {
        //释放锁,通过CAS操作对state进行修改。
        if (tryRelease(arg)) {
            Node h = head;
            //如果head不为null,而且head节点的状态不可能为CANCELLED,head.waitStatus为SIGNAL/PROPAGATE时,
            //说明队列中有正在等待获取锁的节点,需要唤醒后继节点。
            if (h != null && h.waitStatus != 0)
                //唤醒后续节点
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

ReentranLock.Sync.tryRelease(int arg):

//释放锁
protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //如果c=0,说明当前线程完全释放锁,否则只是减少锁的重入次数。
            if (c == 0) {
                free = true;
                //将独占线程置为null
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

        至此,独占锁的获取与释放代码分析完毕。AQS中,最重要的是stat(同步状态),FIFO队列(线程竞争锁的等待队列),这两个值的变化是整个AQS的核心。