AQS源码解析

一、AQS是什么

AQS是AbstractQueuedSynchronizer的简称,它是Java各种锁的底层实现,内部有一个int类型的volatile修饰变量表示同步状态,并提供一系列的CAS操作来管理这个同步状态。

AQS是JAVA中各种锁机制的底层实现,如同步工具类Semaphore、CountDownLatch、ReentrantLock、ReentrantReadWriteLock、FutureTask,CyclicBarrier都是是基于AQS实现的,你也可以基于AQS去实现自己的同步工具类,用法是通过继承AQS实现其模版方法,然后将子类作为同步组件的内部类。

总的来说AQS是一个同步器,采用模板方法设计模式,核心数据结构:双向链表 + state(锁状态),通过CAS底层操作来维护同步状态提供锁机制的底层实现。

为了更好理解AbstractQueuedSynchronizer的运行机制,可以首先研究其内部数据结构,这里整理成如下图:

thread0表示获取锁的线程|thread1表示等待获取锁的线程

AQS内维护一个volatile修饰的int型的变量 state用于记录锁的状态,该值为共享资源>1表示被锁定,state==0表示未被锁定

一个继承自AbstractOwnableSynchronizer类的Thread类型变量exclusiveOwnerThread用于指向当前获取排他锁的线程

两个AbstractQueuedSynchronizer.Node类型的变量head及tail,用来标识队列的头和尾。

这几个字段都用 volatile 关键字进行修饰,以确保多线程间保证字段的可见性。

再细看Node类型为AbstractQueuedSynchronizer的内部类,这个内部类里面对线程进行了封装,并定义了很多属性

1、Node类型

a、mode用来表示队列中个线程要获取锁的类型:
    分为 SHARED(共享)、EXCLUSIVE(排它锁)

b、prev:volatile 修饰的变量,标识当前节点的前驱节点,当前线程依赖它来检查waitStatus,在入队的时候分配

c、next:volatile 修饰的变量,标识当前节点的后继节点,当前线程释放的时候才被唤醒,在入队时分配

2、Thread类型:

    thread:当前节点的线程,在初始化时使用,使用后失效

3、int类型

    waitStatus:标识当前线程等待的状态,对应的值和解释如下

static final int CANCELLED =1;
取消状态,如果当前线程的前置节点状态为 CANCELLED,则表明前置节点已经等待超时或者已经被中断了,这时需要将其从等待队列中删除。

static final int SIGNAL    = -1;
等待触发状态,后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程继续运行,如果当前线程的前置节点状态为 SIGNAL,则表明当前线程需要阻塞

static final int CONDITION = -2;
等待条件状态,表示当前节点在等待 condition,即在 condition 队列中,当其他线程对Condition调用了 signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。

static final int PROPAGATE = -3;
状态需要向后传播,表示下一次共享式同步状态获取将会无条件地被传播下去(可能一个一个节点传播下去),仅在共享锁模式下使用。

AQS是通过维护一个双向链表队列(CLH)实现的,通过锁自旋不断轮询前驱节点的状态,发现前驱释放了锁就结束自旋,

我们来看下它的流程图

各节点自旋获取同步状态

同步队列中首节点是获取到锁的节点,它在释放的时候会唤醒后继节点,后继节点获取到锁的时候,会把自己设为新的首节点。

源码分析:AQS 提供了两种锁,分别是独占锁和共享锁

独占锁指的是操作被认作一种独占操作,比如 ReentrantLock,它实现了独占锁的方法

共享锁则指的是一个非独占操作,比如一些同步工具 CountDownLatch 和 Semaphore 等同步工具,下面是 AQS 对这排它锁提供的抽象方法。

独占锁获取|释放锁的方法

排它锁:以ReentrantLock为例

核心方法 aquaire和release及他们方法体里使用到的方法。

通过 tryAcquire(arg) 方法尝试获取锁,这个方法需要实现类自己实现获取锁的逻辑,获取锁成功后则不执行后面加入等待队列的逻辑了;

如果尝试获取锁失败后,则执行 addWaiter(Node.EXCLUSIVE) 方法将当前线程封装成一个 Node 节点对象,并加入队列尾部;

把当前线程执行封装成 Node 节点后,继续执行 acquireQueued 的逻辑,该逻辑主要是判断当前节点的前置节点是否是头节点,来尝试获取锁,如果获取锁成功,则当前节点就会成为新的头节点。

这个方法首先获取当前线程,再获取当前节点的同步状态c

如果是c=0:0为节点入队时的初始状态,则需进一步调用hasQueuedPredecessors()方法判断这个节点之前是否还有等待获取锁的节点(当前节点是不是需要排队 )。

如果hasQueuedPredecessors返回true,则表示获取锁失败直接返回false

如果hasQueuedPredecessors返回false,则通过CAS操作设置同步状态,再将当前线程设置为获取排他锁的线程,最后返回true表示当前线程获取锁成功。

如果c!=0 ,则进一步判断currentThread==exclusiveOwnerThread,ture则将state同步状态+1,然后设置state返回获取锁成功,false则返回获取锁失败。

注意图中红框出为什么不需要用CAS?因为这里是排它锁,而且当前线程已经获取到锁,所以直接调用setState是线程安全的

看下hasQueuedPredecessors是怎么判断当前节点是怎么判断是否存在等待获取锁的前驱节点的,主要判断当前线程需不需要排队

如果head节点!=tail节点,表示至少有两个节点,进一步如果head节点的下一个节点的线程不是当前线程则表示同步列表中至少存在3个节点,当前线程前面还有线程在等待获取锁。

再来看看当tryAcquire获取锁失败时,调用的addWaiter方法做了些什么

1、新建一个节点封装当前线程

2、判断尾节点是否为空,不为空则将tail节点设置为新节点的前驱节点,通过CAS操作设置tail节点,将原来的tail节点的next指向新的node

3、如果tail节点为空则调用enq方法:enq方法是通过自旋的方式直到新节点设置成功

跟进enq方法

无限循环直至t!=null且成功设置tail节点,下面具体解析下这个方法

如果tail为空则通过CAS的方式设置head,也就是初始化一个同步队列,并将head赋值给tail

下轮循环tail不再为空,则将tail这是为新节点node的前驱节点,在通过CAS操作设置新的tail

最后将原来tail的next指向node并返回。

根据源码分析获取锁失败之后,调用addWaiter创建新节点加到同步队列尾部,在调用acquireQueued方法,这个方法做什么用呢?继续跟进

通过代码可以发现这个方法通过自旋的方式进行处理

1、调用predecessor方法获取当前节点的前驱节点p

2、p==head再次调用tryAcquire尝试获取锁,如果获取成功则将当前节点设置为head,调用p.next=null释放原来的head,当前线程不中断

3、如果p!=head或者tryAcquire获取锁失败,则进行挂起逻辑

注意:head 节点代表当前持有锁的线程,如果当前节点的 pred 节点是 head 节点,很可能此时 head 节点已经释放锁了,所以此时需要再次尝试获取锁

再来看下判断是否需要挂起逻辑的源码

pred为node的pred节点

1、获取ored的等待状态 ws

2、ws==SIGNAL:当前节点需要挂起

3、ws>0:表示pred节点已经被取消,需要在同步队列中剔除,剔除所有取消的节点

4、ws==0||ws==-3:通过CAS将pred的等待状态设置为SIGMAL,再从 acquireQueued 方法自旋操作从新循环一次判断。

继续看挂起逻辑

首先调用LockSupport.park方法阻塞线程,在调用Thread.interrupted方法返回阻塞是否阻塞成功

释放锁:

首先尝试释放锁,如果返回true表示能够释放锁,进一步判断如果head不为空且等待状态!=0则通过unparkSuccessor唤醒后继节点,看下怎么尝试释放锁的

1、获取当前节点的state值-1得到c

2、如果当前线程不是获取排他锁的线程则抛异常

3、如果c==0 标记为可释放,并把当前节点的exclusiveOwnerThread设置为空

4、c!=0,这只state并返回是否可释放的标志

在看下unparkSuccessor的源码

释放锁主要是将头节点的后继节点唤醒,如果后继节点不符合唤醒条件,则从队尾一直往前找,直到找到符合条件的节点为止。

共享锁:以Semaphore为例进行分析

共享锁获取|释放锁

先看下获取共享锁的源码

从代码可以看出也是通过自旋的方式获取锁

首先看当前线程是否需要排队,如果需要排队则返回-1;

不需要排队则获取当前节点的state值

判断余量<0?  CAS设置同步状态并返回余量值remaining,返回值小于0 获取共享锁失败,调用doAcquireShared(arg)方法,接着往下看

1、增加一个SHARED类型的节点,并添加到同步队列的尾部

2、获取node的pred节点p

3、p==head,再次尝试获取共享锁:成功则调用setHeadAndPropagate方法设置头并传播唤醒的动作.

4、将p.next=null,释放

5、如果p不是头节点,则执行阻塞逻辑和排它锁逻辑一样,不再赘述

跟进setHeadAndPropagate方法

1、将老的head复制给h

2、设置新的头节点

3、如果propagate>0 标识需要传播唤醒;h==null||h.waitStatus<0;新的头为null||新的头的waitStatus<0 ; 获取node的后继节点s,s==null||s的获取锁的模式是SHARED则调用doReleaseShared方法下面是该方法的源码

依然是通过自旋来处理的,当h==head时跳出循环

head不为空&&head!=tail时获取head的waitStatus值ws

1、ws==SIGNAL 则将节点的SIGNAL设置为0,如果设置失败则进入下轮循环

2、设置成功则调用unparkSuccessor唤醒后继节点

3、ws==0,则将节点的等待状态设置为PROPAGATE,表示可以向后继续传播,如果设置失败一直循环直到成功

释放共享锁

首先尝试释放共享锁,返回true则调用doReleaseShared()方法上面已经解析过了我们看下tryReleaseShared方法的代码

1、获取当前节点的同步状态 state

2、重置信号量,设置成功则返回true,否则一直循环直到成功

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349