第一次写博客,先练练手。

1.AQS是什么?

在Lock中,用到了一个同步队列AQS,全称为AbstractQueuedSynchronizer,它是一个同步工具也是lock用来实现线程同步的核心组件

2.AQS的两种功能

从使用层面来说,AQS的功能分为两种:独占和共享

独占锁,每次只能有一个线程持有锁,比如ReentrantLock就是以独占方式实现的互斥锁

共享锁,允许多个线程同时获取锁,并发访问共享资源,比如ReentrantReadWriteLock

3.AQS的内部实现

AQS队列内部维护的是一个FIFO的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接的前驱节点。

所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。

每个Node其实是由线程封装,当线程抢锁失败后会封装成Node加入到AQS队列中去,当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点(线程).

Node 的组成:

 static final class Node {
/**
* Marker to indicate a node is waiting in shared mode
*/
static final Node SHARED = new Node();
/**
* Marker to indicate a node is waiting in exclusive mode
*/
static final Node EXCLUSIVE = null; /**
* waitStatus value to indicate thread has cancelled
*/
static final int CANCELLED = 1;
/**
* waitStatus value to indicate successor's thread needs unparking
*/
static final int SIGNAL = -1;
/**
* waitStatus value to indicate thread is waiting on condition
*/
static final int CONDITION = -2; static final int PROPAGATE = -3; volatile int waitStatus; volatile Node prev;//前驱节点 volatile Node next;//后继节点 volatile Thread thread;//当前线程 Node nextWaiter;//存储在condition队列中的后继节点 /**
* 是否为共享锁
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
} final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null) {
throw new NullPointerException();
} else {
return p;
}
} Node() { // Used to establish initial head or SHARED marker
}

     //将线程构造一个Node,添加到等待队列中
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
//这个方法会在Codition队列使用
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}

  

4 释放锁以及添加线程对于队列的变化

当出现锁竞争以及释放锁的时候,AQS同步队列中的节点会发生变化,首先看一下添加节点的场景

这里会设计到两个变化:

1)新的线程封装成Node节点追加到同步队列中,设置pre节点以及修改当前节点的前置节点的next节点指向自己

2)通过CAS将tail重新指向新的尾部节点

head节点表示获取锁成功的节点,当头节点在释放同步状态时,会唤醒后继节点,如果后继节点获取锁成功,会把自己设置为头结点,节点变化如下:

这个过程涉及两个变化

1)修改head节点指向下一个获取锁的节点

2)新的获取锁的节点,将prev的指针指向null

注意:

设置head节点不需要CAS,原因是设置head节点是由获取锁的线程来完成的,而同步锁只能由一个线程获取,所以不需要CAS保证,

只需要把head节点设置为原首节点的后继节点,并且断开原head节点的nest引用即可

5 以ReentrantLock 为例分析AQS源码

ReentrantLock的时序图如下:

ReentrantLock.lock()

这个是reentrantLock获取锁的入口

public void lock() {
sync.lock();
}

  sync实际上是一个抽象的静态内部类,它继承了AQS来实现重入锁的逻辑,我们前面说过AQS是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备业务功能,所以在不同的同步场景中,会继承AQS来实现对应场景的功能。

Sync有两个具体的实现类 ,分别是:

NonfairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁

FairSync:表示所有线程严格按照FIFO来获取锁

NonfairSync.lock

以非公平锁为例,来看看lock中的实现

1) 非公平锁与公平锁最大的区别在于,在非公平锁中,抢占锁的逻辑是,不管有没有线程排队,我上来先cas去抢占一下

2) CAS成功,就表示成功获得了锁

3)CAS失败,调用acquire(1) 走锁竞争逻辑

final void lock() {
if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}

CAS的实现原理:

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

通过cas乐观锁的方式来做比较并替换,这段代码的意思是,如果当前内存中的state的值和预期值expect相等,则替换为update,更新成功返回true,否则返回false。

这个操作是原子的,不会出现线程安全问题,这里面涉及到Unsafe这个类的操作,以及涉及到state这个属性的意义。

state是AQS中的一个属性,它在不同的实现中所表达的含义不一样,对于重入锁的实现来说,表示一个同步状态,它有两个含义:

1) 当state=0时,表示无锁状态

2)当state>0 时,表示已经有线程获取了锁,也就是state=1,但是因为ReentrantLock允许重入,所以同一个线程多次获得同步锁的时候,state会递增,比如重入5次,那么state=5,而在释放锁的时候,同样需要释放5次直到state=0,其他线程才有资格获取锁。

Unsafe 类:

Unsafe 类是在sun.misc包下,不属于Java标准,当时很多java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty,Hadoop,Kafka等。

Unsafe可认为是一个Java中留下的后门,提供了一个底层次操作,如直接内存访问,线程的挂起和恢复,cas,线程同步,内存屏障

而cas就是Unsafe类中提供的一个原子操作,第一个参数为需要改变的对象,第二个为偏移量(即之前求出来的headOffset的值),第三个参数为期待的值,第四个为更新后的值,整个方法的作用就是如果当前时刻的值等于预期值var4相等,则更新为新的期望值var5,如果更新成功,则返回true,否则返回false。

stateOffset:

一个java对象可以看成一段内存,每个字段都得按照一定的顺序放在这段内存里,通过这个方法可以准确的告诉你某个字段相对于对象的起始内存地址的字节偏移。用于后面的compareAndSwapInt中,去根据偏移量找到对象在内存中的具体位置。所以stateOffset表示state这个字段在AQS类中内存中相对于该类首地址的偏移量。

AQS.acquire
      acquire 是AQS中的操作,如果cas操作未能成功,说明state已经不为0,此时继续acquire(1)操作
       思考: acquire方法中的1 的参数是用来做什么的?
    这个方法的主要逻辑:
    1)通过tryAcquire尝试获取独占锁,如果成功返回true,失败返回false
    2)如果tryAcquire失败,则会通过addWaiter方法将当前线程封装成Node添加到AQS队列尾部
     3)acquireQueued ,将Node作为参数,通过自旋去尝试获取锁

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
NonfairSync.tryAcquire
    这个方法的作用是尝试获取锁,如果成功返回true,不成功返回false,它是重写AQS类中的tryAcquire方法,并且大家仔细看一下AQS中tryAcquire方法的定义,并没有实现,而是抛出异常,按照一般的思维模式,既然是一个不实现的模板方法,那应该定义成abstract,让子类来实现啊,想想为啥?
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
ReentrantLock.nofairTryAcquire

final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();//获取当前执
行的线程
int c = getState();//获得 state 的值
if (c == 0) {//表示无锁状态
if (compareAndSetState(0, acquires)) {//cas 替换 state 的
值,cas 成功表示获取锁成功
setExclusiveOwnerThread(current);//保存当前获得锁的线
程,下次再来的时候不要再尝试竞争锁
return true;
}
}
else if (current == getExclusiveOwnerThread()) {//如果同一
个线程来获得锁,直接增加重入次数
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false; }

1)获取当前线程,判断当前的锁状态

2)如果state=0表示当前是无锁状态,通过cas更新state状态的值

3)当前线程是属于重入,则增加重入次数

AQS.addWaiter

当tryAcquire方法获取锁失败以后,则会先调用addWaiter将当前线程封装成Node,入参mode表示当前节点的状态,传递的参数是Node。EXCLUSIVE,表示独占状态,意味着重入锁用到了AQS的独占锁功能

    1)将当前线程封装成Node
    2)当前链表中的tail节点是否为空,如果不为空,则通过cas操作把当前线程的node添加到AQS队列
    3) 如果为空或者cas失败,调用enq将节点添加到AQS队列

private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);//把
当前线程封装为 Node
Node pred = tail; //tail 是 AQS 中表示同比队列队尾的属性,默认
是 null
if (pred != null) {//tail 不为空的情况下,说明队列中存在节点
node.prev = pred;//把当前线程的 Node 的 prev 指向 tail
if (compareAndSetTail(pred, node)) {//通过 cas 把 node
加入到 AQS 队列,也就是设置为 tail
pred.next = node;//设置成功以后,把原 tail 节点的 next
指向当前 node
return node;
}
}
enq(node);//tail=null,把 node 添加到同步队列
return node;
}

enq

enq就是通过自旋操作把当前节点加入到队列中

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

图解分析

假设3个线程来争抢锁,那么截止到enq方法运行结束之后,或者调用addWaiter方法结束后,AQS中的链表结构图:

AQS.acquireQueued
 
    通过addWaiter方法把线程添加到链表后,会接着把Node作为参数传递给acquireQueued方法,去竞争锁
    1)获取当前节点的prev节点
     2)如果prev节点为head节点,那么它就有资格去争抢锁,调用tryAcquire抢占锁
     3)抢占锁成功以后,把获得锁的节点设置为head,并且移除原来的初始化head节点
     4)如果获取锁失败,则根据waitStatus决定是否需要挂起线程
      5)最后通过cancelAcquire取消获取锁的操作
final boolean acquireQueued(final Node node, int
arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();//获
取当前节点的 prev 节点
if (p == head && tryAcquire(arg)) {//如
果是 head 节点,说明有资格去争抢锁
setHead(node);//获取锁成功,也就是
ThreadA 已经释放了锁,然后设置 head 为 ThreadB 获得执行权

p.next = null; //把原 head 节点从链表中
移除
failed = false;
return interrupted;
}//ThreadA 可能还没释放锁,使得 ThreadB 在执
行 tryAcquire 时会返回 false
if (shouldParkAfterFailedAcquire(p,
node) &&
parkAndCheckInterrupt())
interrupted = true; //并且返回当前线程
在等待过程中有没有中断过。
}
} finally {
if (failed)
cancelAcquire(node);
} }
shouldParkAfterFailedAcquire
 
   如果 ThreadA 的锁还没有释放的情况下,ThreadB 和 ThreadC 来争抢锁肯定是会失败,那么失败以后会调用 shouldParkAfterFailedAcquire 方法
   Node 有 5 中状态,分别是:CANCELLED(1),SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、默认状态(0)

       CANCELLED: 在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该 Node 的结点, 其结点的 waitStatus 为 CANCELLED,即结束状态,        进入该状态后的结点将不会再变化
      SIGNAL: 只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程
     CONDITION: 和 Condition 有关系,后    续会讲解
       PROPAGATE:共享模式下,PROPAGATE 状态的线程处于可运行状态
       0:初始状态
        这个方法的主要作用是,通过 Node 的状态来判断,ThreadA 竞争锁失败以后是
          否应该被挂起。
                1. 如果 ThreadA 的 pred 节点状态为 SIGNAL,那就表示可以放心挂起当前线程
                2. 通过循环扫描链表把 CANCELLED 状态的节点移除
                 3. 修改 pred 节点的状态为 SIGNAL,返回 false. 返回 false 时,也就是不需要挂起,返回 true,则需要调用 parkAndCheckInterrupt
                  挂起当前线程

private static boolean
shouldParkAfterFailedAcquire(Node pred, Node
node) {
int ws = pred.waitStatus;//前置节点的
waitStatus
if (ws == Node.SIGNAL)//如果前置节点为 SIGNAL,意
味着只需要等待其他前置节点的线程被释放,
return true;//返回 true,意味着可以直接放心的挂
起了
if (ws > 0) {//ws 大于 0,意味着 prev 节点取消了排
队,直接移除这个节点就行
do {
node.prev = pred = pred.prev;
//相当于: pred=pred.prev;
node.prev=pred;
} while (pred.waitStatus > 0); //这里采用循
环,从双向列表中移除 CANCELLED 的节点
pred.next = node;
} else {//利用 cas 设置 prev 节点的状态为 SIGNAL(-
1)
compareAndSetWaitStatus(pred, ws,
Node.SIGNAL);
}
return false; }
parkAndCheckInterrupt
 

使用 LockSupport.park 挂起当前线程编程 WATING 状态Thread.interrupted,返回当前线程是否被其他线程触发过中断请求,也就是
thread.interrupt(); 如果有触发过中断请求,那么这个方法会返回当前的中断标识true,并且对中断标识进行复位标识已经响应过了中断请求。如果返回 true,意味着在 acquire 方法中会执行 selfInterrupt()。
 
    private final boolean parkAndCheckInterrupt() {
      LockSupport.park(this);
    return Thread.interrupted();
     }
 
selfInterrupt: 标识如果当前线程在 acquireQueued 中被中断过,则需要产生一个中断请求,原因是线程在调用 acquireQueued 方法的时候是不会响应中断请求的
    static void selfInterrupt() {
          Thread.currentThread().interrupt();
      }
 
  图解分析
         通过acquireQueued方法来竞争锁,如果ThreadA还在执行中没有释放锁的话,意味着ThreadB和ThreadC只能挂起
       

LockSupport

LockSupport类是Java6引入的一个类,提供了基本的线程同步原语,LockSupport实际上是调用了Unsafe类里的函数,归结到Unsafe里,只有两个函数,

uppark函数为线程提供“许可(permit)” ,线程调用park函数则等待"许可",有点像信号量,但是这个许可 是不能重叠的, 许可是一次性的

permit相当于0/1的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit,又变成0,如果再调用一次park会阻塞,因为permit已经是0了,直到permit变成1,这时调用unpark会把permit设置为1,每个线程相关的permit,最多只有一个,重复调用unpark不会累积。

锁释放流程

如果这个时候ThreadA释放锁了,那么我们来看锁被释放后会产生什么效果

ReentrantLock.unlock
             在unlock中,会调用release方法释放锁

public final boolean release(int arg) {
if (tryRelease(arg)) { //释放锁成功
Node h = head; //得到 aqs 中 head 节点
if (h != null && h.waitStatus != 0)//如果 head 节点不
为空并且状态!=0.调用 unparkSuccessor(h)唤醒后续节点
unparkSuccessor(h);
return true;
}
return false; }
ReentrantLock.tryRelease
    这个方法可以认为是一个设置锁状态的操作,通过将state状态减掉传入的参数值 1,如果结果状态为1,就将排他锁的Owner设置null,便于其他的线程有机会进行执行,
   在排它锁中,加锁的状态的时候回增加1 ,在解锁的时候减掉1,同一个锁,在可以重入后,可能会被叠加2,3,4这些值,只有unlock的次数和lock的次数对应才会将owner线程设置为空,而且也只有这种情况下才会返回true

protected final boolean tryRelease(int releases)
{
int c = getState() - releases;
if (Thread.currentThread() !=
getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
unparkSuccessor 

private void unparkSuccessor(Node node) {
int ws = node.waitStatus;//获得 head 节点的状态
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);// 设置 head 节点
状态为 0
Node s = node.next;//得到 head 节点的下一个节点
if (s == null || s.waitStatus > 0) {
//如果下一个节点为 null 或者 status>0 表示 cancelled 状态. //通过从尾部节点开始扫描,找到距离 head 最近的一个
waitStatus<=0 的节点
s = null;
for (Node t = tail; t != null && t != node; t =
t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null) //next 节点不为空,直接唤醒这个线程即可
LockSupport.unpark(s.thread);
}

为什么在释放锁的时候是从tail进行扫描

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

1)将新的节点的prev指向tail

2)通过cas将tail设置为新的节点,因为cas是原子操作所以能够保证线程安全性

3)t.next=node,设置原tail的next节点指向新的节点

在cas操作之后,t.next=node操作之前,存在其他线程调用unlock方法从head开始往后遍历,由于t.next=node还没执行意味着链表的关系还没有建立完整,就会导致遍历到t节点的时候被中断,所以从后往前遍历,一定不会存在这个问题

原本挂起的线程继续执行

通过ReentrantLock.unlock,原本挂机的线程被唤醒以后会继续执行,原来被挂起的线程是在acquireQueued方法中,所以被唤醒以后继续从这个方法开始执行。

    公平锁和非公平锁的区别:
      锁的公平性是相对于获取锁的顺序而言,如果一个公平锁,那么锁的获取顺序就应该符合请求的绝对时间顺序,也就是FIFO,只要cas设置同步状态成功,则表示当前线程获取了锁,而公平锁不一样。
 
 

死磕abstractqueuedsynchronizer源码的更多相关文章

  1. 死磕Spring源码之AliasRegistry

    死磕Spring源码之AliasRegistry 父子关系 graph TD; AliasRegistry-->BeanDefinitionRegistry; 代码实现 作为bean定义的最顶层 ...

  2. 【死磕jeestie源码】类型后面三个点(String...)和数组(String[])的区别

    类型后面三个点(String...),是从Java 5开始,Java语言对方法参数支持一种新写法,叫可变长度参数列表,其语法就是类型后跟...,表示此处接受的参数为0到多个Object类型的对象,或者 ...

  3. 死磕itchat源码--core.py

    core.py文件中的Core类定义了itchat的所有接口.且,仅仅是定义了接口,全部在component包中实现重构.其用法如下表述: 缺省 源码如下: # -*- encoding: utf-8 ...

  4. 死磕itchat源码--config.py

    itchat的配置文件,源码: import os, platform # 版本及微信的url,二维码等 VERSION = '1.3.10' BASE_URL = 'https://login.we ...

  5. 死磕itchat源码--content.py

    content.py中定义了接受消息的类型,即,用于注册消息函数时的参数类型.源码如下: TEXT = 'Text' MAP = 'Map' CARD = 'Card' NOTE = 'Note' S ...

  6. 死磕itchat源码--__init__.py

    itchat包中的__init__.py是该库的入口:在该文件中的源码如下: # -*- coding: utf-8 -*- from . import content from .core impo ...

  7. 死磕itchat源码--目录结构

    阅读itchat源码时,先弄清itchat的目录结构 itchat │ config.py │ content.py │ core.py │ log.py │ returnvalues.py │ ut ...

  8. 【死磕jeesite源码】Jeesite配置定时任务

    一.主要是注意XML文件中设置3个地方和类文件中配置 第一步配置: 第二步配置:注解扫描 第三步配置:开启任务 类中注解配置:如下 @Service 或者Component @Lazy(false) ...

  9. 【死磕jeesite源码】jeesite添加多数据源

    本文转载自jeesite添加多数据源 1.jeesite.properties 添加数据源信息,(url2,username2,pawwword2) #mysql database setting j ...

随机推荐

  1. 「疫期集训day4」硝烟

    那真是一阵恐怖的炮击(that boomed booms),响亮的炮音(that noise),滚滚的硝烟(that smoke),熊熊的火焰在围绕着我们前进...小心前进(go and be car ...

  2. Numerical Sequence(hard version),两次二分

    题目: 题意: 已知一个序列: 112123123412345123456123456712345678123456789123456789101234567891011... 求这个序列第k个数是多 ...

  3. 从零开始学Electron笔记(二)

    在之前的文章我们简单介绍了一下Electron可以用WEB语言开发桌面级应用,接下来我们继续说一下Electron的菜单创建和事件绑定. 我们接上一章的代码继续编写,上一章代码 https://www ...

  4. MYSQL 之 JDBC(十四):批量处理JDBC语句提高处理效率

    1.当需要成批插入或者更新记录时.可以采用java的批量更新机制,这一机制允许多条语句一次性提交给数据库批量处理.通常情况下比单独提交处理更有效率. 2.JDBC的批量处理语句包括下面两个方法: ad ...

  5. Producter and Consumer

    package pinx.thread; import java.util.LinkedList; import java.util.Queue; public class ProducerConsu ...

  6. 微信小程序动态修改title,动态配置title,动态配置头部,微信小程序动态配置头部

    微信小程序的title是在json里面配置的 "navigationBarTitleText": "title名称" 这种title是固定死的不灵活处理一些页面 ...

  7. 数据规整:连接、联合与重塑知识图谱-《利用Python进行数据分析》

    所有内容整理自<利用Python进行数据分析>,使用MindMaster Pro 7.3制作,emmx格式,源文件已经上传Github,需要的同学转左上角自行下载或者右击保存图片. 其他章 ...

  8. 重装win7时遇到点小问题

         最近装系统的时候有个头疼的事,事情的起因是这样的,我在工作的时候用的win7,破解的时候各种工具都破解不了,说是有未分配的盘符.并且,当时装的是没更新的win7,工作上要用到ie11只能在w ...

  9. 细说php锁

    bool flock ( int handle, int operation [, int &wouldblock] );flock() 操作的 handle 必须是一个已经打开的文件指针.o ...

  10. 关于python爬取异步ajax数据的一些见解

    我们在利用python进行爬取数据的时候,一定会遇到这样的情况,在浏览器中打开能开到所有数据,但是利用requests去爬取源码得到的却是没有数据的页面框架. 出现这样情况,是因为别人网页使用了aja ...