AQS详解之独占锁模式

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,失败返回falsetryRelease(int)//独占方式释放资源,成功返回true,失败返回falsetryAcquireShared(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()方法.每一次释放锁就会唤醒队列中该节点的后继节点,可以进一步的说明获取锁是一个先进先出的过程.

原文地址:https://www.cnblogs.com/fengyun2050/p/12384588.html

时间: 2024-10-07 10:10:36

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

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

背景:之前在研究多线程的时候,模模糊糊知道AQS这个东西,但是对于其内部是如何实现,以及具体应用不是很理解,还自认为多线程已经学习的很到位了,贻笑大方. 这里的源码还没有能够完整的跟下来,后面还要继续努力完善. Java并发包基石-AQS详解 Java并发包(JUC)中提供了很多并发工具,这其中,很多我们耳熟能详的并发工具,譬如ReentrangLock.Semaphore,它们的实现都用到了一个共同的基类--AbstractQueuedSynchronizer,简称AQS.AQS是一个用来构建

sqlserver锁机制详解(sqlserver查看锁)

简介 在SQL Server中,每一个查询都会找到最短路径实现自己的目标.如果数据库只接受一个连接一次只执行一个查询.那么查询当然是要多快好省的完成工作.但对于 大多数数据库来说是需要同时处理多个查询的.这些查询并不会像绅士那样排队等待执行,而是会找最短的路径执行.因此,就像十字路口需要一个红绿灯那 样,SQL Server也需要一个红绿灯来告诉查询:什么时候走,什么时候不可以走.这个红绿灯就是锁. 图1.查询可不会像绅士们那样按照次序进行排队 为什么需要锁 在开始谈锁之前,首先要简单了解一下事

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

原文来自于:http://www.ituring.com.cn/article/128439 Windows下的PHP开发环境搭建——PHP线程安全与非线程安全.Apache版本选择,及详解五种运行模式. 今天为在Windows下建立PHP开发环境,在考虑下载何种PHP版本时,遭遇一些让我困惑的情况,为了解决这些困惑,不出意料地牵扯出更多让我困惑的问题. 为了将这些困惑一网打尽,我花了一下午加一晚上的时间查阅了大量资料,并做了一番实验后,终于把这些困惑全都搞得清清楚楚了. 说实话,之所以花了这么

Java并发包基石-AQS详解

目录 1 基本实现原理 1.1 如何使用 1.2 设计思想 2 自定义同步器 2.1 同步器代码实现 2.2 同步器代码测试 3 源码分析 3.1 Node结点 3.2 独占式 3.3 共享式 4 总结 Java并发包(JUC)中提供了很多并发工具,这其中,很多我们耳熟能详的并发工具,譬如ReentrangLock.Semaphore,它们的实现都用到了一个共同的基类--AbstractQueuedSynchronizer,简称AQS.AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效

图灵学院Java架构师-VIP-并发编程(AQS详解)

1.LockSupport LockSupport类的核心方法其实就两个:park()和unark(),其中park()方法用来阻塞当前调用线程,unpark()方法用于唤醒指定线程 LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,可以把许可看成是一种(0,1)信号量(Semaphore),但与 Semaphore 不同的是,许可的累加上限是1. 初始时,permit为0,当调用unpark()方法时,线程的permit加1,当调用park()方法时,

Synchronized&Lock&AQS详解

加锁目的:由于线程执行的过程是不可控的,所以需要采用同步机制来协同对对象可变状态的访问. 加锁方式:java锁分为两种--显示锁和隐示锁,本质区别在于显示锁需要的是程序员自己手动的进行加锁与解锁如ReentrantLock需要进行lock与unlock.而隐式锁则是Synchronized,jvm内置锁,jvm进行操作加锁与解锁. Synchronized关键字 每个对象创建后都会存在一个Monitor(监视器锁),它的实现依赖底层的系统的Mutex Lock(互斥锁)实现,是重量级锁,但是在j

MySQL详解--锁

来源:http://blog.csdn.net/xifeijian/article/details/20313977#t4 锁是计算机协调多个进程或线程并发访问某一资源的机制.在数据库中,除传统的计算资源(如CPU.RAM.I/O等)的争用以外,数据也是一种供许多用户共享的资源.如何保证数据并发访问的一致性.有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素.从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂.本章我们着重讨论MySQL锁机制的特点,常见的锁

面向接口编程详解(三)——模式研究

通过前面两篇,我想各位朋友对“面向接口编程”的思想有了一定认识,并通过第二篇的例子,获得了一定的直观印象.但是,第二篇中的例子旨在展示面向接口编程的实现方法,比较简单,不能体现出面向接口编程的优势和这种思想的内涵.那么,这一篇作为本系列的终结篇,将通过分析几个比较有深度的模式或架构,解析隐藏其背后的面向接口思想.这篇我将要分析的分别是MVC模式和.NET平台的分层架构. 这篇的内容可能会比较抽象,望谅解. 1.从MVC开始 MVC简介: 本文不打算详细解释MVC架构,而是把重点放在其中的面向接口

MySQL中锁详解(行锁、表锁、页锁、悲观锁、乐观锁等)

原文地址:http://blog.csdn.net/mysteryhaohao/article/details/51669741 锁,在现实生活中是为我们想要隐藏于外界所使用的一种工具.在计算机中,是协调多个进程或线程并发访问某一资源的一种机制.在数据库当中,除了传统的计算资源(CPU.RAM.I/O等等)的争用之外,数据也是一种供许多用户共享访问的资源.如何保证数据并发访问的一致性.有效性,是所有数据库必须解决的一个问题,锁的冲突也是影响数据库并发访问性能的一个重要因素.从这一角度来说,锁对于