Java并发之AQS原理详解

标签: AQS  同步框架 并发 原理解析
17人阅读 评论(0) 收藏 举报
分类:

一、AbstractQueuedSynchronizer的用途(下面简称AQS,jdk 1.8)
AQS是借助FIFO等待队列,用来实现同步器的同步框架,通俗的来说,它是用来实现锁的工具,一般来说,它需要实现这些功能:
  • 让得到锁的线程继续向下执行,让没有得到锁的线程阻塞,需要时唤醒等待的线程;
  • 根据锁的用途,制定相应地获取锁的策略,比如互斥锁的话必须保证同一时刻只有一个线程可以得到锁,共享锁的话保证满足条件下,一到多个线程可以获得锁。

二、AQS的工作原理

AQS的核心是基于链表实现的FIFO等待队列,该队列用于存放需要等待的线程节点,AQSNode内部类用于保存等待线程的相关信息。

1、  等待队列结构

工作机制:AQS的等待队列是基于链表实现的FIFO的等待队列,队列每个节点只关心其前驱节点的状态,线程唤醒时只唤醒队头等待线程(即head的后继节点,并且等待状态不大于0)。

                

2、  Node内部类

Node内部类用于保存等待线程的相关信息

主要的数据成员有:

static final Node SHARED;//表明该节点在共享模式下等待;

static final Node EXCLUSIVE;//表明该节点在互斥模式下等待;

volatile int waitStatus;//该节点的等待状态:1 CANCELLED,-1 SIGNAL, -2 CONDITION, -3 PROPAGATE, 0 others

volatile Node prev;//该节点的前驱节点

volatile Node next;//该节点的后继节点

volatile Thread thread;//该节点关联的线程

主要的方法成员有:

final boolean isShared();//当前节点是否在共享模式下等待;

final Node predecessor();// 返回当前节点的前驱节点; 

3、  AQS的数据成员

private transient volatile Node head;//等待队列的头节点,该节点的等待状态不为CANCELLED

private transient volatile Node tail;//等待队列的尾节点;

private volatile int state;//同步器的状态;

4、  AQS方法成员之节点基本操作

注:只写了一些比较主要的,像setHead(), hasQueuedThreads(), hasContented(), ….可以直接查看源码,比较简单

将线程作放入等待队列中private Node addWaiter(Node mode);

功能:以指定模式创建当前线程的Node,并添加到等待队列中,返回添加的节点。

具体操作:

  • 如果队列为空的话,调用enq()完成队列添加;
  • 否则的话直接添加到等待队列中,并更新尾节点。

源码:

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;

    }

private Node enq(final Node node);

功能:将node添加到等待队列中,一直循环直到添加成功

具体操作:

  • for循环中一直执行如下步骤;
  • 如果等待队列为空,通过CAS操作将队列头节点更新为空节点;
  • 通过CAS操作将尾节点更新为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;

                }

            }

        }

    }

 唤醒给定节点的后继节点private void unparkSuccessor(Node node);

具体操作:
  • 如果node的等待状态小于0的话,通过CAS将其更新为0
  • 找到node节点等待状态不大于0的后继节点,唤醒该后继节点。

源码:

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

 

        /*

         * 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 t = tail; t != null && t != node; t = t.prev)

                if (t.waitStatus <= 0)

                    s = t;

        }

        if (s != null)

            LockSupport.unpark(s.thread);

    }

 取消获取锁 private void cancelAcquire(Node node);

具体操作:

  • 如果node为空,直接返回;
  • 找到node节点的前驱节点pred(跳过取消的前驱节点),和后继节点next,更新node
  • 如果pred是头节点的话,唤醒node的后继节点,如果node是尾节点的话,用CAS操作更新尾节点;
  • 否则的话,用CAS操作将pred的后继节点更新为next节点。

源码:

private void cancelAcquire(Node node) {

        // Ignore if node doesn't exist

        if (node == null)

            return;

 

        node.thread = null;

 

        // Skip cancelled predecessors

        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.

        node.waitStatus = Node.CANCELLED;

 

        // If we are the tail, remove ourselves.

        if (node == tail && compareAndSetTail(node, pred)) {

            compareAndSetNext(pred, predNext, null);

        } else {

            // If successor needs signal, try to set pred's next-link

            // so it will get one. Otherwise wake it up to propagate.

            int ws;

            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

        }

    }

 private static booleanshouldParkAfterFailedAcquire(Node pred, Node node);

功能:获取锁失败后检查并更新节点的状态,如果线程应该阻塞的话,返回true 

具体操作:

  • 如果pred的等待状态为SIGNAL,说明pred节点正在等待被唤醒,因此node可以安全阻塞,返回true;
  • 如果pred的等待状态大于0,说明该节点已经取消等待了,更新node节点的前驱节点(跳过取消的前驱节点),返回false
  • 否则pred的等待状态为0或者PROPAGATE,表明需要等待,但是还没阻塞,因此用CAS操作将pred的等待状态更新为SIGNAL,返回false  

源码:

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.

             */

            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

        }

        return false;

    }

注:对于该方法的功能,我是这样理解的,

应用场景:

该方法经常用于阻塞线程的方法中(比如acquireQueued(finalNode node, int arg), doAcquireInterruptibly(int arg)等),并且经常在for死循环中重复调用。

情形一:如果node节点的前驱节点的等待状态为SIGNAL,说明前驱节点正在等待获取锁,node节点肯定不能直接获取锁的,必须等待,因此可以安全阻塞,返回true,可以在阻塞线程的方法中将当前线程挂起;

情形二:如果node节点的前驱节点的等待状态为CANCELLED,更新node节点的前驱节点(跳过取消的前驱节点),返回false。更新完成后,node节点的前驱节点有可能就是head节点,即node节点可以直接尝试获取锁,因此在退出该方法后在阻塞线程的方法中重新执行循环;

情形三:否则的话,node节点的前驱节点的等待状态为0或者PROPAGATE,将前驱节点的等待状态更新为SIGNAL,返回false后,在阻塞线程的方法中重新执行循环就可以达到情形一。

 private final booleanparkAndCheckInterrupt()

功能:park一个线程,当线程被unparkh后,返回中断状态

源码:

private final boolean parkAndCheckInterrupt() {

        LockSupport.park(this);

        return Thread.interrupted();

    }

方法很简单,简要说一下:

  • 用park()方法挂起的线程可以主要可以通过unpark()或者interrupt方法唤醒,其中interrupt()调用后,线程的中断状态会发生变化,而通过unpark()方法唤醒,线程的中断状态并不会发生变化;
  • 因此可以通过parkAndCheckInterrupt()方法的返回值,判断线程是通过unpark()还是通过interrupt()唤醒的,如果是interrupt()唤醒的,可以通过抛出异常、捕获异常的方式来达到等待队列中的节点可以取消获取锁的目的,详细可见doAcquireInterruptibly(int arg)方法。

5、  AQS方法成员之获取锁的操作

说明:

  • 获取锁的操作主要可以分为两大类:共享式获取和独占式获取,每一类又可以进一步地细分为:可响应中断的获取、不可响应中断的获取以及计时获取;
  • 共享式获取和独占式获取的主要区别在于调用的获取锁的方法不同,共享式获取锁调用的是tryAcquireShared(int arg)方法,而独占式获取锁调用的是tryAcquire(intarg)方法,而这两种方法都是需要AQS的继承类复写的(想详细了解的话可以看下ReentrantLockSemaphore,前者是以独占模式下获取锁的,后者是以共享模式获取锁的);
  • 可响应中断的获取与不可响应中断的获取的主要区别在于可响应中断的获取会在当通过中断将线程从park()中唤醒后,显式地抛出异常,并在finally语句中捕获异常,一种常见线程响应中断的处理是:该线程调用cancelAcquire(Node node)来取消获取锁。

因为原理都差不多,因此主要讲两个方法:非中断模式下独占式获取锁public final void acquire(int arg)和中断模式下独占式获取锁publicfinal void acquireInterruptibly(int arg) 

非中断模式下独占式获取锁public final void acquire(int arg) 

具体操作:

  • 调用tryAcquire(intarg)尝试获取锁,获取锁成功的话,线程继续执行;
  • 否则的话调用addWaiter(Nodenode)方法将该线程以独占模式添加到等待队列;
  • 然后调用acquireQueued(Nodenode,int arg)阻塞线程,只有队列头节点可以尝试获取锁,其他节点必须等待。

  源码:

public final void acquire(int arg) {

        if (!tryAcquire(arg) &&

            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

            selfInterrupt();

    }

      final boolean acquireQueued(final Node node, int arg)

     功能:非中断模式下独占式获取锁,返回值为等待获取锁的过程中该线程是否被中断

     具体操作:

  •   for死循环中执行如下操作;
  • 获得node节点的前驱节点p,如果p为头节点的话,则尝试获得锁,获得失败的话,重新执行循环;成功的话返回interrupted
  • 调用shouldParkAfterFailedAcquire()方法判断节点是否可以安全阻塞,不可以的话重新执行for循环;
  • 否则调用parkAndCheckInterrupt()方法将线程挂起,当线程的从park()中唤醒后,如果是以中断唤醒的,将interrupted的值置为true,不论以哪种方式唤醒,均需重新执行循环。

解释:该方法会阻塞线程,直到线程成功获得锁,返回interrupted。期间如果遇到中断的话,仅仅是将interrupted值置为true。也就是说不论是否中断,等待队列中的线程不可以取消获取锁,获取锁成功后才会从等待队列中移除。

源码:

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

        }

    }

 中断模式下独占式获取锁public final void acquireInterruptibly(int arg) 

具体操作:

  • 如果线程的中断状态为true的话,直接抛出中断异常;
  • 如果线程获取锁成功的话,不需要阻塞,直接返回;
  • 否则调用doAcquireInterruptibly(intarg)方法让线程以可中断模式阻塞。     

源码:

public final void acquireInterruptibly(int arg)

            throws InterruptedException {

        if (Thread.interrupted())

            throw new InterruptedException();

        if (!tryAcquire(arg))

            doAcquireInterruptibly(arg);

    }

 private voiddoAcquireInterruptibly(int arg)

 功能:线程以可中断的互斥模式下获取锁

 具体操作:

  •  调用addWaiter(Nodenode),将当前线程以互斥模式添加到等待队列中;
  • for死循环中执行下列操作;
  • 获得node节点的前驱节点p,如果p为头节点的话,则尝试获得锁,获得失败的话,重新执行循环;成功的话返回;
  • 调用shouldParkAfterFailedAcquire()方法判断节点是否可以安全阻塞,不可以的话重新执行for循环;
  • 否则调用parkAndCheckInterrupt()方法将线程挂起,当线程的从park()中唤醒后,如果是以unpark()方法唤醒的,重新执行循环;
  • 否则说明是以中断方式唤醒的,显式抛出中断异常,调用cancelAcquire(Node node)方法取消获取锁。

解释:该方法会阻塞线程,线程要想从该方法中返回的有两种途径,一是成功获取锁,二是遇到中断,如果入到中断的话会取消获取锁。

6、  AQS方法成员之释放锁的操作

释放锁分类同获取锁,独占式地释放锁调用tryRelease()方法,共享式释放锁调用tryReleaseShared()方法,共享式释放锁会一直尝试释放锁,直到释放成功;独占式释放锁不一定可以成功释放锁,释放成功返回true,失败返回false

源码不讲了,思路很简单,通过调用unparkSuccessor(Node node)方法唤醒等待队列中最靠前且未取消的节点。

7、  AQS方法成员之必须重写的方法

protected boolean tryAcquire(int arg);

protected int tryAcquireShared(int arg)

protected boolean tryRelease(int arg);

protected boolean tryReleaseShared(int arg);

protected boolean isHeldExclusively()

三、AQS的执行流程
以获取锁为例:


 

 

 

查看评论

Win32汇编教程二 Win32汇编程序的结构和语法

Win32汇编程序的结构和语法--------------------------------------------------------------------------------Win32...
  • ghj1976
  • ghj1976
  • 2000-12-15 08:34:00
  • 2784

聊聊高并发(二十一)解析java.util.concurrent各个组件(三) 深入理解AQS(一)

AQS是AbstractQueuedSynchronizer的缩写,AQS是Java并包里大部分同步器的基础构件,利用AQS可以很方便的创建锁和同步器。它封装了一个状态,提供了一系列的获取和释放操作,...
  • ITer_ZC
  • ITer_ZC
  • 2014-11-06 14:16:49
  • 4565

java aqs原理浅析

前言aqs是指java.util.concurrent.locks包下的类AbstractQueuedSynchronizer,这个类是java.util.concurrent包的基础,同步工具类Se...
  • chenwanli1111
  • chenwanli1111
  • 2016-04-10 22:27:53
  • 1591

【死磕Java并发】-----J.U.C之AQS:AQS简介

Java的内置锁一直都是备受争议的,在JDK 1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6后,进行大量的锁优化策略(【死磕Java并发】—–深入分析synchr...
  • chenssy
  • chenssy
  • 2017-03-05 22:26:36
  • 7113

Java并发框架——什么是AQS框架

什么是AQS框架 1995年sun公司发布了第一个java语言版本,可以说从jdk1.1到jdk1.4期间java的使用主要是在移动应用和中小型企业应用中,在此类领域中基本不用设计大型并发场景,当然...
  • wangyangzhizhou
  • wangyangzhizhou
  • 2014-11-09 23:04:57
  • 4086

java并发-AQS.ObjectCondition源码解析

1 什么是条件队列        它使得一组线程能够通过某种方式来等待特定的条件变成真,条件队列的元素是一个个正在等待状态的线程。对象的内置锁(synchronized语义对应的同步机制),关联着一个...
  • wojiushiwo945you
  • wojiushiwo945you
  • 2014-12-29 15:43:15
  • 3124

剖析基于并发AQS的共享锁的实现(基于信号量Semaphore)

【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/76167357 出自【...
  • javazejian
  • javazejian
  • 2017-07-31 09:33:37
  • 6925

jdk1.8 J.U.C并发源码阅读------AQS之conditionObject内部类分析

jdk1.8中java.util.concurrent包中源码阅读笔记。
  • u011470552
  • u011470552
  • 2017-08-02 10:10:15
  • 634

Java并发框架——AQS阻塞队列管理(一)——自旋锁

我们知道一个线程在尝试获取锁失败后将被阻塞并加入等待队列中,它是一个怎样的队列?又是如何管理此队列?这节聊聊CHL Node FIFO队列。  在谈到CHL Node FIFO队列之前,我们先分析这种...
  • wangyangzhizhou
  • wangyangzhizhou
  • 2014-12-20 22:58:45
  • 3559

Java并发框架——AQS之阻塞与唤醒

根据前面的线程阻塞与唤醒小节知道,目前在Java语言层面能实现阻塞唤醒的方式一共有三种:suspend与resume组合、wait与notify组合、park与unpark组合。其中suspend与r...
  • wangyangzhizhou
  • wangyangzhizhou
  • 2014-12-13 21:21:16
  • 2370
    个人资料
    等级:
    访问量: 2万+
    积分: 413
    排名: 11万+
    最新评论