AQS介绍

AbstractQueuedSynchronizer简称AQS,即队列同步器。它是JUC包下面的核心组件,它的主要使用方式是继承,子类通过继承AQS,并实现它的抽象方法来管理同步状态,它分为独占锁和共享锁。很多同步组件都是基于它来实现的,比如我门常见的ReentrantLock,它是基于AQS的独占锁实现的,它表示每次只能有一个线程持有锁。在比如ReentrantReadWriteLock它是基于AQS的共享锁实现的,它允许多个线程同时获取锁,并发的访问资源。AQS是建立在CAS上的一种FIFO的双向队列,它通过维护一个int类型的state,这个state是用volatile来修饰,从而保证状态的安全行。

AQS对于状态的更改提供了3个方法:

  1. getState() :返回同步状态的当前值

  2. setState() : 设置当前同步状态

  3. compareAndSetState():使用CAS设置当前状态,该方法能够保证状态的原子性。它是通过Unsafe这个类中的native方法来保证的。

如果请求的共享资源空闲,那么就把当前请求的线程设置为工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源占用,那么需要一套线程阻塞等待以及唤醒的锁的分配机制。那么这套机制AQS是用CLH队列锁实现的,获取不到锁的线程将加入到队列中。AQS内部维护的一个同步队列,获取失败的线程会加入到队列中进行自旋,移除队列条件是前驱节点是头节点并且成功获取到了同步状态,释放同步状态AQS会调用unparkSuccessor方法唤醒后继节点。

AQS队列内部维护的是一个FIFO的双向链表,如下图。这种结构的特点是每个数据结构都有2个指针,分别指向直接前驱节点和直接的后继节点。这种结构可以从任意的一个节点开始很方便的访问前驱和后继节点。每个Node由线程封装,当竞争失败后会加入到AQS队列

下面具体看一下Node组成:

static final class 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值,表示下一个共享模式应该无条件传播*/
   static final int PROPAGATE = -3;
   /**状态字段*/
   volatile int waitStatus;
   /**前驱节点 */
   volatile Node prev;
   /**后继节点 */
   volatile Node next;
   /**当前线程*/
   volatile Thread thread;
   /**将此节点入列的线程,用来指向下一个节点*/
   Node nextWaiter;
   /**如果节点在共享模式下等待,则返回true*/
   final boolean isShared() {
       return nextWaiter == SHARED;
  }
   /**返回上一个节点,如果为null则抛出异常,前驱节点不是null使用 */
   final Node predecessor() throws NullPointerException {
       Node p = prev;
       if (p == null)
           throw new NullPointerException();
       else
           return p;
  }
   Node() {    // 用于建立初始化head节点
  }
   Node(Thread thread, Node mode) {     // 由addWaiter使用
       this.nextWaiter = mode;
       this.thread = thread;
  }
   Node(Thread thread, int waitStatus) { // 由Condition使用
       this.waitStatus = waitStatus;
       this.thread = thread;
  }
}

加入队列的过程必须是线程安全的,所以AQS提供了一个基于CAS设置尾节点的方法compareAndSetTail,这个也是unsafe类中的native方法。它需要传入当前线程的认为的尾节点和当前节点,当设置成功后,当前节点和尾部节点建立关联,当前节点正式加入到队列。

出队列设置头节点也必须要要保证线程安全的,AQS是通过CAS设置头节点的compareAndSetHead这个方法来实现的。此方法也是unsafe类型中的native方法。

AQS重要方法

AQS使用了模版方法模式,自定义同步器需要重写下面的几个AQS提供的模版方法:

isHeldExclusively()//该线程是否处于独占资源。只有用到condition才需要实现它.
tryAcquire(int)//独占方式获取资源,成功返回true,失败返回false
tryRelease(int)//独占方式释放资源,成功返回true,失败返回false
tryAcquireShared(int)//共享方式获取资源。负数表示失败,0表示成功但是没有剩余可用资源;正数表示成功且有剩余资源
tryReleaseShared(int)//共享方式释放资源.成功返回true,失败返回false.

独占锁的获取是通过AQS提供的acquire()。我门看一下这个方法的源代码:

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

发现acquire()获取同步状态成功与否做了2件事情。1成功,方法结束返回,2失败,会将当前线程加入到同步队列,它是通过调用addWaiter()和acquireQueued()方法实现的,我门继续看一下这2个方法的源代码.

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;
      }
  }
   enq(node);
   return node;
}

通过方法会发现它会先把当前线程封装为Node类型,然后判断尾节点是否为空,如果不为空进行CAS操作入队列,如果为空,那么会调用enq()这个方法,此方法做了通过不断的for循环自旋CAS尾插入节点。

现在我门已经明白独占锁获取失败入队列的过程了,那么对于同步队列的节点会做什么事情来保证自己有机会获取独占锁呢?我门来看一下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; // help GC
               failed = false;
               return interrupted;
          }
           if (shouldParkAfterFailedAcquire(p, node) &&
               parkAndCheckInterrupt())//获取失败调用的方法
               interrupted = true;
      }
  } finally {
       if (failed)
           cancelAcquire(node);
  }
}

从源代码我门可以看出来这是一个自旋过程(for(;;)),它首先获取当前节点的前驱节点,然后判断当前节点能否获取独占锁,如果前驱节点是头节点并且获取同步状态,那么就可以获取到独占锁。如果获取锁失败线程会进入等待状态等待获取独占锁。

shouldParkAfterFailedAcquire()这个方法主要的逻辑是调用compareAndSetWaitStatus(),使用CAS将节点状态由INITIAL设置为SIGNAL。如果失败会返回false,通过acquireQueued()的自旋转会继续设置,直到设置成功。设置成功后调用parkAndCheckInterrupt()方法,此方法会调用LockSupport.park(this)让该线程阻塞。到此独占锁获取过程已经分析完毕了。

独占锁的释放是用relase()方法,我门来看一下源代码

public final boolean release(int arg) {
   if (tryRelease(arg)) {
       Node h = head;
       if (h != null && h.waitStatus != 0)
           unparkSuccessor(h);
       return true;
  }
   return false;
}

这段代码的逻辑就很容易理解了,如果同步状态释放成功,则执行if语句内的代码,当head不为空并且状态不为0的时候会执行unparkSuccessor()方法,unparkSuccessor方法会执行LookSupport.unpark()方法.每一次释放锁就会唤醒队列中该节点的后继节点,可以进一步的说明获取锁是一个先进先出的过程.

AQS详解之独占锁模式的更多相关文章

  1. AQS详解

    一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronized(AQS)! 类如其名,抽象的队列式的同步器,AQ ...

  2. Java并发之AQS详解

    一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)! 类如其名,抽象的队列式的同步器,AQ ...

  3. (转)Java并发包基石-AQS详解

    背景:之前在研究多线程的时候,模模糊糊知道AQS这个东西,但是对于其内部是如何实现,以及具体应用不是很理解,还自认为多线程已经学习的很到位了,贻笑大方. Java并发包基石-AQS详解Java并发包( ...

  4. 【1】AQS详解

    概述: 它内部实现主要是状态变量state和一个FIFO队列来完成,同步队列的头结点是当前获取到同步状态的结点,获取同步状态state失败的线程,会被构造成一个结点加入到同步队列尾部(采用自旋CAS来 ...

  5. Java并发之AQS详解(转)

    一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronized(AQS)! 类如其名,抽象的队列式的同步器,AQ ...

  6. Java AQS详解(转)

    原文地址 一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)! 类如其名,抽象的队列式的同 ...

  7. AQS详解,并发编程的半壁江山

    千呼万唤始出来,终于写到AQS这个一章了,其实为了写这一章,前面也是做了很多的铺垫,比如之前的 深度理解volatile关键字 线程之间的协作(等待通知模式) JUC 常用4大并发工具类 CAS 原子 ...

  8. 转:Windows下的PHP开发环境搭建——PHP线程安全与非线程安全、Apache版本选择,及详解五种运行模式。

    原文来自于:http://www.ituring.com.cn/article/128439 Windows下的PHP开发环境搭建——PHP线程安全与非线程安全.Apache版本选择,及详解五种运行模 ...

  9. 深入浅出AQS之独占锁模式

    每一个Java工程师应该都或多或少了解过AQS,我自己也是前前后后,反反复复研究了很久,看了忘,忘了再看,每次都有不一样的体会.这次趁着写博客,打算重新拿出来系统的研究下它的源码,总结成文章,便于以后 ...

随机推荐

  1. 用Java中的File类模拟实现对系统文件的增删改查效果

    码字不易,三连支持一波吧 IO操作向来是各大语言的热区,而对文件的操作也是重中之重. 那么在Java中也给我们提供了很多关于文件操作的类.今天我就用一个比较基本的File类来模拟实现对文件的增删改查效 ...

  2. python的format

    python的format 就是一个参数传递+格式化的过程 参数传递 1.位置传递,默认 fmt = "{} {}" fmt.format("hello",&q ...

  3. Redis 源码简洁剖析 13 - RDB 文件

    RDB 是什么 RDB 文件格式 Header Body DB Selector AUX Fields Key-Value Footer 编码算法说明 Length 编码 String 编码 Scor ...

  4. 关于 share 音乐分享官方文档补充

    例子 音乐分享 /*--------微信朋友圈--------*/ [shareParams SSDKSetupWeChatParamsByText:@"内容" title:@&q ...

  5. 【Java分享客栈】我为什么极力推荐XXL-JOB作为中小厂的分布式任务调度平台

    前言   大家好,我是福隆苑居士,今天给大家聊聊XXL-JOB的使用.   XXL-JOB是本人呆过的三家公司都使用到的分布式任务调度平台,前两家都是服务于传统行业(某大型移动基地和某大型电网),现在 ...

  6. [SuperSocket2.0]SuperSocket 2.0从入门到懵逼

    SuperSocket 2.0从入门到懵逼 SuperSocket 2.0从入门到懵逼 1 使用SuperSocket 2.0在AspNetCore项目中搭建一个Socket服务器 1.1 引入Sup ...

  7. RainbowCrack彩虹表破解密码hash

    实验目的 使用彩虹表破解散列值b0baee9d279d34fa1dfd71aadb908c3f 实验原理 1)彩虹表破解是利用彩虹表破解散列数据的工具. 这种方法不同于暴力破解攻击.暴力破解攻击会将密 ...

  8. 深度学习分类问题中accuracy等评价指标的理解

    在处理深度学习分类问题时,会用到一些评价指标,如accuracy(准确率)等.刚开始接触时会感觉有点多有点绕,不太好理解.本文写出我的理解,同时以语音唤醒(唤醒词识别)来举例,希望能加深理解这些指标. ...

  9. 今儿直白的用盖房子为例,给你讲讲Java建造者模式

    摘要:建造者模式(Builder Pattern)又叫生成器模式,是一种对象构建模式.它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象. 本 ...

  10. C#索引器-有参属性

    总结 只要类中有类似于属性的元素就应创建索引器,此属性代表的不是一个值,而是值的集合,其中每一个项由一组参数标识. 这些参数可以唯一标识应引用的集合中的项. 索引器延伸了属性的概念,索引器中的一个成员 ...