并发编程(八)AQS-抽象队列同步器-ReetrantLock加锁解锁过程

AQS

Java并发编程的核心在于java.util.concurrent包。而juc当中大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer,简称AQS。AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器。

AQS具备特性:

    • 阻塞等待队列
    • 共享/独占
    • 公平/非公平
    • 可重入
    • 允许中断

这些特性是怎么实现的,ReentrantLock为例

    • 一般通过定义内部类Sync [sɪŋk] 继承AQS
    • 将同步器所有调用都映射到Sync对应的方法

 

AQS框架 - 管理状态

    1. AQS内部维护属性:volatile int state(32位)
      state表示资源的可用状态
    2. state三种访问方式
      getState()、setState()、compareAndSetState()
    3. AQS定义两种资源共享方式
      Exclusive 独占,只有一个线程能执行,如ReetrantLock
      Share 共享,多个线程可以同时执行,如Semaphore/CountDownLatch
    4. AQS定义两种队列
      ● 同步等待队列 ● 条件等待队列

框架

 

 

 

 它维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。这里volatile是核心关键词,具体volatile的语义,在此不述。state的访问方式有三种:

  • getState()
  • setState()
  • compareAndSetState()

  AQS定义两种资源共享方式:Exclusive(独占,只有一个线程能执行,如ReentrantLock)和Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

  不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

  • isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

  以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

  再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

  一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。

 源码分析

AQS内部有一个节点Node,这个节点存储处于等待的线程:Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。

  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。

  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

  • 0:新结点入队时的默认状态。

注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常

ReentrantLock

是独占锁,使用AQS来实现lock,unlock的语义。

 其内部结构:

 

我们看到ReentrantLock内部定义了抽象类Sys,它继承了AbstractQueuedSynchronizer但是只实现了必要的tryRelease,isHeldExclusively。对于获取锁的方式tryAcquire它又分了另外两个内部类继承Sys来实现。根据获取锁的方式不同,

 ReentrantLock,可以有公平锁和非公平锁的区分,公平锁就是FairSync,意思就是如果一个线程在获取锁的时候会首先判断queue中是否已经有在等待的线程了,如果有的话就直接入队尾不再尝试获取锁。而非公平锁则不然,它是不判断queue中是否有等待的线程上来就自己先尝试获取锁,如果这时候锁刚好释放它是有机会拿到锁的,而那些等待中的线程还要继续等待所以称为非公平锁。这个锁实现的细节下面会 看到。

 

 

  ReentrantLock提供了两个构造函数,默认是非公平的,可以传入boolean值来决定是用公平的还是非公平的。

 private static ReentrantLock lock=   new ReentrantLock();
    private static int num=0;
    public static void main(String[] args) {
        for (int i = 0; i <3 ; i++) {
            Thread thread=new Thread(()->{
               try{
                   lock.lock();
                   for (int j = 0; j <1000 ; j++) {
                      num++;
                   }
               }finally {
                   lock.unlock();
               }
            });
            thread.start();
        }
        try {
            Thread.sleep(6000L);
            System.out.println(num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

 

我们默认使用非公平的锁,开启三个线程我们跟着Debug进去看下, 

 

 先跟Thread-0:

1: 用的是非公平的锁

 //Acquires the lock
 public void lock() {
        sync.lock();
    }

 

 

 2:非公平锁上来就尝试获取锁:compareAndSetState(0, 1)

/**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

 

 cas操作把AQS中的state从0更新为1,如果成功当前线程获取到锁。这个地方Thread0会获取锁成功。

   protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

 

 获取到锁之后把当前线程设置为独占线程

/**
 * Sets the thread that currently owns exclusive access.
 * A {@code null} argument indicates that no thread owns access.
 * This method does not otherwise impose any synchronization or
 * {@code volatile} field accesses.
 * @param thread the owner thread
 */
protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

 

 Thread0就开始执行for循环的逻辑了。我让Thread0挂起到for循环执行中来看Thread1的行为。

Thread1:

Thread1执行到上面lock的时候,尝试获取锁失败,因为Thread0还在占用。会执行:acquire(1); 方法。这是AQS中的方法:

 /**
     * Acquires in exclusive mode, ignoring interrupts.  Implemented
     * by invoking at least once {@link #tryAcquire},
     * returning on success.  Otherwise the thread is queued, possibly
     * repeatedly blocking and unblocking, invoking {@link
     * #tryAcquire} until success.  This method can be used
     * to implement method {@link Lock#lock}.
     *
     * @param arg the acquire argument.  This value is conveyed to
     *        {@link #tryAcquire} but is otherwise uninterpreted and
     *        can represent anything you like.
     */
    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();//  因为parkAndCheckInterrupt方法里判断是否中断的方法会清除中断标志位,所以这里如果是中断的话会再次主动中断。
}

 

 tryAcquire 由子类实现,这里就是NonfairSync.

这里做的主要事情就是:

1:虽然这个逻辑是由获取锁失败才走到的,但是第一步这里还是会尝试获取一次锁。tryAcquire

 protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
 /**
         * Performs non-fair tryLock.  tryAcquire is implemented in
         * subclasses, but both need nonfair try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState(); 
            if (c == 0) {  //查看锁的状态,如果这时候锁释放了,就CAS获取锁,获取成功返回true
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);  //获取成功之后设置独占锁
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) { //  如果锁状态不为0,说明锁被占着,查看是不是当前线程占着的,设置的独占线程这时候用上了。
                int nextc = c + acquires;//   如果独占线程就是当前线程,进行锁重入的逻辑,就是把state状态加1
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

 

2:tryAcquire返回false,尝试获取锁失败,进行下一步,addWaiter(Node.EXCLUSIVE) ,参数是一个null的Node。

/**
     * Creates and enqueues node for current thread and given mode.
     * 根据给的mode创建node节点和队列并进行入队
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new 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; //第一次的时候尾结点 tail 为空
        if (pred != null) {
            node.prev = pred; // 尾结点不为空,新节点的前驱节点指向pred
            if (compareAndSetTail(pred, node)) { // cas设置尾结点为新节点
                pred.next = node;//  pred.next指向新节点,和 node.prev = pred 构成双向链表
                return node; 
}
}
enq(node);// 初始化队列 再入队
return node;
}

 

 /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {
        for (;;) {   //  cas 进行初始化并入队
            Node t = tail;
            if (t == null) { // Must initialize   第一次循环的走这里
                if (compareAndSetHead(new Node()))
                    tail = head;  // 初始化队列,头尾节点相同
            } else {
                node.prev = t;   // 第二次循环的时候执行  node 入队尾
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

 

3: acquireQueued(addWaiter(Node.EXCLUSIVE), arg)   ,addWaiter把Node入队之后,接下来就是把当前线程挂起了,具体过程如下:

 final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;   // 线程是否被中断,
            for (;;) {  //  CAS 
                final Node p = node.predecessor();  // 获取当前节点的前驱节点
                if (p == head && tryAcquire(arg)) {  // 前驱节点是头结点的情况下再次尝试获取锁
                    setHead(node);         //  获取锁成功之后,把当前节点设置为头结点,而且在setHead方法中 把节点的thread=null,prev=null
                    p.next = null; // help GC 失去GCRoot
                    failed = false;
                    return interrupted;
                }
// 如果没有获取到锁 或node的前驱节点不是头结点, 就把当前线程进行阻塞,但是阻塞之前会进行前驱节点状态的判断和设置。
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())// 线程挂起之后阻塞在这里,唤醒之后再进行CAS判断,如果获取不到锁继续park。继续park的时候,会再次把前驱节点从0改为1,因为在unpartk的时候前驱节点从-1被改为0了,就是为了在这里
// 用于判断unpark的线程可能获取锁还是失败,需要再次partk interrupted
= true; } } finally { if (failed) cancelAcquire(node); } }

 

 

 

shouldParkAfterFailedAcquire(p, node) /**
     * Checks and updates status for a node that failed to acquire.
     * Returns true if thread should block. This is the main signal
     * control in all acquire loops.  Requires that pred == node.prev.
     *
     * @param pred node's predecessor holding status
     * @param node the node
     * @return {@code true} if thread should block
     */
    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.
能够正常挂起(park)的线程它的前驱节点的状态必须是Node。SIGNAL
*/ return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry.
如果前驱节点的线程状态大于0,说明线程被取消了,从后向前一直找到状态<=0的节点,然后把当前节点放它后面
*/ do { node.prev = pred = pred.prev; //当pred.waitStatus>0,把pred的前驱赋给pred,并把当前节点的前驱指向他 } 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.
根据注释也可以知道waitStatus的状态是0/PROPAGATE的
//如果前驱正常,那就把前驱的状态设置成SIGNAL
队列中的节点都要先经过这一步把waitStatus从0变成-1,后面的节点才能正常被唤醒。
*/ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }

 当前驱节点状态正常shouldParkAfterFailedAcquire返回true之后就可以parkAndCheckInterrupt()挂起线程了。

 /**
     * Convenience method to park and then check if interrupted
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);// 挂起当前线程 但是这里传入了一个参数,还有一个无参的park()方法,有参的这个可以做到如果线程是被中断唤醒的,下次循环到这行代码还是会被挂起,如果是无参的park,被中断唤醒后再次走到这里,就不会再被挂起了
// 如果想要被挂起,只能调用Thread.interrupted()方法,清除掉中断标志位,这样就又可以被挂起了。
return Thread.interrupted(); // 线程唤醒之后,判断是否是被中断唤醒的,因为park阻塞的线程一种可以通过unpark方式唤醒,还有一种就是中断唤醒,但是这里虽然返回了中断标识,但是调用了这个方法之后会清除中断标识位
// 所以在外面要想判断线程是否是被中断唤醒的就要有个变量判断下,而且在acquire方法中调用了主动中断的方法。 }

 

 获取锁的过程分析完了,解锁的过程如下:

public void unlock() {
        sync.release(1);
    }
// 这是AQS中的方法,tryRelease是独占锁实现的方法,最开始我们知道Sync里面已经实现了
public
final boolean release(int arg) { if (tryRelease(arg)) {
// 完全释放锁之后进行唤醒其它线程 Node h
= head;
//unpark的时候会把头节点的waitStatus从-1变成0
if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }

 

 

protected final boolean tryRelease(int releases) {
            int c = getState() - releases; // 因为锁可以重入,而且state是多大表示重入了几次,所以一次解锁1.
            if (Thread.currentThread() != getExclusiveOwnerThread())//  会判断解锁的线程是不是当前持有锁的独占线程,如果不是会抛出异常
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {  // 如果减去1之后不为0,说明锁没有完全释放
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

 

 

 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)
            compareAndSetWaitStatus(node, ws, 0);  // 把头节点的状态从负值变成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;
//如果头节点的下一个节点状态不合法就从尾节点开始向前查找第一个状态<=0的节点进行唤醒
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); }

 还有一种获取锁的方式:

public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

 

 public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }
 /**
     * Acquires in exclusive interruptible mode.
     * @param arg the acquire argument
     */
    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        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;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
// 如果是被中断唤醒的,直接抛出中断的异常 不再进行处理
throw new InterruptedException(); } } finally { if (failed)
// 被中断唤醒之后 设置waitStatus为 1 并剔除 cancelAcquire(node); } }

 

 

分析过程告一段落,如有错误欢迎指正。

 有篇不错的AQS文章推荐下:https://www.cnblogs.com/waterystone/p/4920797.html

posted @ 2021-04-26 23:32  蒙恬括  阅读(178)  评论(0编辑  收藏  举报