其实AQS并不难

不啰嗦,直接上干货

文章目录

      • 上锁
      • 解锁
      • 总结
      • 条件队列 newCondition
      • CLH队列的数据结构
      • 扩展 interrupted

上锁

 ReentrantLock reentrantLock = new ReentrantLock(true);
 或者
 ReentrantLock reentrantLock = new ReentrantLock();
 看构造函数:
 //无参的构造函数,默认为非公平锁
 public ReentrantLock() {
        sync = new NonfairSync();
    }
 //通过你的传参,创建公平锁或者非公平锁
 public ReentrantLock(boolean fair) {
       sync = fair ? new FairSync() : new NonfairSync();
   }

在这里以非公平锁为例,因为公平锁用到的方法在非公平锁中都有。
先从上锁开始,万事开头难,先把上锁搞定,后面就顺理成章了:

reentrantLock.lock();

lock方法在抽象类abstract static class Sync extends AbstractQueuedSynchronizer中定义的抽象方法,所以具体实现为为子类,跟踪后,是在ReentrantLock$NonfairSync内部类中实现。

ReentrantLock$NonfairSync{
	final void lock() {
		//通过cas算法获取锁,获取到锁则设置当前线程为当前拥有独占访问权限的线程,
		//这就是非公平锁体现的地方,上了就竞争锁,没抢到就被加入到链表队列中,只要进入到链表队列中就只能是公平获取了
	    if (compareAndSetState(0, 1))
	        setExclusiveOwnerThread(Thread.currentThread());
	    else
	    	//否则竞争锁
	        acquire(1);
	}
}


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

tryAcquire在父类中定义了,但是是在ReentrantLock$NonfairSync内部类中被重写,所以调用的是内部类中的tryAcquire方法。好戏从这才开始

ReentrantLock$NonfairSync{
	protected final boolean tryAcquire(int acquires) {
       return nonfairTryAcquire(acquires);
    }
}


final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    //state表示当前是否被上锁,0:未上锁,1:上锁
    int c = getState();
    if (c == 0) {
    	//若为0,则再次尝试获取锁,并设置独占锁线程为当前线程
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    //如果getState!=0,则表示当前被上锁,则判断此时独占锁是否为当
    //前线程,若相等,则次数加1,如同sync锁的monitor+1。
    //(重入锁的体现)
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //都不是,则返回false,表示未抢到锁
    return false;
}

通过源码知道了tryAcquire只是通过cas方法区获取锁,如果获取不到锁则返回false。再看上面的代码(我写到这吧),

public final void acquire(int arg) {
	//tryAcquire(arg)获取不到锁,则返回false,!false=true,则开始
	//进行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法,
	//首先进入addWaiter方法
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

//创建一个属性值为当前线程的【独占模式】的节点
/**
 1. Node.EXCLUSIVE独占锁,EXCLUSIVE=null,也就是说同时也指定了下一个等待节点为null
 2. Node.SHARED共享锁,创建读锁时使用,平常一般都是独占锁,与重入锁是两个概念
 */
private Node addWaiter(Node mode) {
   Node node = new Node(Thread.currentThread(), mode);
    //获取未新增节点前的尾部节点,并且将其作为即将要插入节点的前一节点
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        //利用cas算法进行插入,如果插入失败则调用下面的enq方法
        //compareAndSetTail也是只比较并交换,自身无自旋
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果pred为null,则表示链表队列为空,则执行enq方法新建链表队列
    enq(node);
    return node;
}

//其实这个方法与addWaiter方法没啥区别,就是多了一个两个判断,
//并且添加了for(;;),进行自旋
private Node enq(final Node node) {
    for (;;) {
    	//获取尾部节点
        Node t = tail;
        //如果尾部节点为空,则设置【新建】线程为空的头部节点,且为尾部节点(循环链表),for循环就会执行else操作
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
        	//此时尾部节点已不为空,重新设置尾部节点为当前要插入节点,并插入链表队列中
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

看了上面的源码,得知addWaiter这个方法,就是将当前节点设置为尾部节点(为啥不设置为头部节点,反而将头部节点thread设置为空??)然后返回。

//node=通过上面的分析,node节点已创建,为尾部节点
//arg=1
final boolean acquireQueued(final Node node, int arg) {
   boolean failed = true;
   try {
   	   //标记当前线程是否需要中断
       boolean interrupted = false;
       //死循环,表示自旋
       //上面介绍的tryAcquire使用了cas:compareAndSwapInt算法,
       //cas:compareAndSwapInt这个算法只是进行比较并替换,本身并没有自旋,但是unsafe类中提供的getAndAddInt、getAndAddLong等api,它们本身才带有自旋,compareAndSwapInt这个只是交换
       for (;;) {
       	   //获取该节点上一节点
           final Node p = node.predecessor();
           //如果p是head节点,就再次尝试获取锁,这里再次尝试获取
           //锁的原因是如果线程在前面没有获取锁,但是没有立即park,而是入队列,
           //所以就想着万一在入队的过程中释放了锁呢,所以在这里再获取一次锁
           //还有另一层意思就是,非公平锁时,一上来就会获取锁,如果锁被新来的线程获取到了呢,所以这里是尝试获取锁,而不是直接获取锁
           if (p == head && tryAcquire(arg)) {
           //前置节点为头部节点,并且获取到锁,则说明头部节点已经执行完毕,并释放锁。
           	   //setHead将当前节点设置为头部节点,并将thread属性置位null,如同上面的enq新建的头部节点格式。(当前线程已经被setExclusiveOwnerThread里面了)
               setHead(node);
               //既然获取到了锁,那么也就说明p已经执行完了,那么就要从链表中删除,所以p.next=null
               p.next = null; // help GC
               //取消获取锁的标记为false
               failed = false; //TODO 没搞懂
               return interrupted;
           }
           //如果上一步失败,则进行此操作
           //shouldParkAfterFailedAcquire从方法名上我们可以大概猜出这是判断是否要阻塞当前线程的,这是一个【核心】,看下面的说明
           if (shouldParkAfterFailedAcquire(p, node) &&
               parkAndCheckInterrupt())
               interrupted = true;
       }
   } finally {
       if (failed)
           cancelAcquire(node);
   }
}

shouldParkAfterFailedAcquire方法是核心方法
Node.SIGNAL = -1,表示当前线程正在阻塞。而这个方法的逻辑是将前置节点的waitStatus置为-1,因为当前节点自己不会主动设置为-1,而是依赖下一个节点,让下一个节点将当前节点设置为-1,为什么要这么做呢,需要结合parkAndCheckInterrupt方法一起分析,看逻辑:

//pred为当前节点上一节点,node为当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	//获取前置节点的ws,因为每次新建的节点waitStatus都为0,所以
	//执行此方法时,第一次只会执行最后的else操作,然后返回
	//false,又因为上游是for(;;)循环,所以当再次调用的时候,执行if操作,返回true,然后执行parkAndCheckInterrupt方法,
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)//SIGNAL -1
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) { //状态为CANCELLED
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
         //则一直往链表队列头部回溯直到找到一个状态不为CANCELLED的结点,将当前节点node挂在这个结点的后面。
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else { //状态为初始化状态(ReentrentLock语境下)
        /*
         * 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.
         */
         //修改前置节点的waitStatus从0改为Node.SIGNAL -1。
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

//shouldParkAfterFailedAcquire()返回为true时,则说明前置节点ws=-1,然后调用此方法
private final boolean parkAndCheckInterrupt() {
   //直接阻塞当前线程,直到另一个线程调用unpark(A)时被唤醒
   LockSupport.park(this);
   //返回此线程线程中断标识,false
    return Thread.interrupted();
}

当前节点阻塞,为啥不自己修改ws为-1呢,为啥要依赖下一个节点进行修改呢,有两个原因:

  1. 如果在阻塞前设置了当前节点的ws=-1,但是因为线程调度的关系,还没来得及执行park,这岂不是乱套了。
  2. 如果在阻塞后设置为ws=-1,那更不行,阻塞后设置,说明已经不是阻塞状态了,再设置状态为阻塞状态,更不合适了

正是因为以上两个原因,所以才添加了以上shouldParkAfterFailedAcquire方法,当前节点设置前置节点为-1阻塞状态。
至此,获取锁的逻辑到此为止,只能等待被另一个线程调用unpark(threadId)进行唤醒,所以parkAndCheckInterrupt方法,只会在唤醒后才会被执行完,也就是说上锁的过程已经完了。

解锁

结算就容易点了,下面代码是整个调用链,

public void unlock() {
    sync.release(1);
}

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

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

private void unparkSuccessor(Node node) {
   int ws = node.waitStatus;
   if (ws < 0)
       compareAndSetWaitStatus(node, ws, 0);
   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);
}

代码就是这样,关键最后一行LockSupport.unpark(s.thread);唤醒下一个节点,那么也就是从parkAndCheckInterrupt方法开始执行呗,进行下一次循环获取锁,未获取到锁则继续进行阻塞。

总结

AQS的队列,头部节点的thread永远是空的,即使是初始化的时候,因为队列当中的head永远是持有锁的那个node,队列当中的除了head之外的所有的node都在park,当释放锁之后unpark(唤醒)下一个node(有时候也不会是第二个,比如第二个被cancel之后,至于为什么会被cancel,不在我们讨论范围之内,cancel的条件很苛刻,基本不会发生),node被唤醒,node被设置为head,在sethead方法里面会把node的Thread设置为null,为什么需要设置null?其实原因很简单,现在t2已经拿到锁了,node就不要排队了,那么node对Thread的引用就没有意义了。所以队列的head里面的Thread永远为null。

条件队列 newCondition

条件队列,大概原理如下:

Condition testConditon = lock.newCondition();
//如果链表队列不为空,则初始化一个链表队列,
//并将当前线程创建成一个节点,追加队尾
testConditon.await();
//将等待队列中的节点,循环取出,添加到同步队列中。
testConditon.signalAll();
  1. await的大概原理就是,await被调用时,则将当前线程加入到链表对列中,如果链表队列为空,以当前线程创建的节点为firstWaiter的循环链表。
  2. signalAll的大概原理就是,将await创建的等待队列,进行循环,将每个节点添加到同步队列中。

也就是说一个创建链表队列,一个将队列中的节点取出。程序中
定义多个newCondition,逻辑之间相互调用,效果如同synchronize锁的wait/notifyAll方法,但不同的是,wait/notifyAll
只能阻塞/唤醒一个线程,而reentrantlock的条件对列,可以为多个线程

AQS
state
head
tail
内部类 Node{
waitStatus(node节点状态)
mode(node模式,独占、共享)
}
嵌套类ConditionObject{
//条件队列使用
}
等其他属性。

CLH队列的数据结构

在这里插入图片描述

扩展 interrupted

interrupted只是表示这个线程是否中断的意思,只是表示的意思,也就是说,程序员可以根据interrupted值来判断自己的逻辑是否需要被中断,比如:

@Test
public void testIntreupted(){
     System.out.println(Thread.currentThread().isInterrupted());
     Thread.currentThread().interrupt();
     System.out.println(Thread.currentThread().isInterrupted());
     if(Thread.currentThread().isInterrupted()){
		//do anything
	}
 }
 output:
  false
  true

也就是说interrupt并不是暂停线程,而只是表示你应该暂停了,给程序一个温柔的反应,比方说,你某个逻辑,不能立即暂停,程序员可以通过这个方法来决定你的逻辑是不是应该暂停。

热门文章

暂无图片
编程学习 ·

springcloud config 配置访问

springcloud http请求地址和资源文件映射如下: / { 应用名 } / { 环境名 } [ / { 分支名 } ] / { 应用名 } - { 环境名 }.yml / { 应用名 } - { 环境名 }.properties / { 分支名 } / { 应用名 } - { 环境名 }.yml / { 分支名 } / { 应用名 } - { 环境名 }.properties label 分支…
暂无图片
编程学习 ·

C++数据结构第16课、线性表存储结构的抽象实现

课程目标 — 完成顺序存储结构线性表的抽象实现SeqList 设计要点 — 抽象类模板,存储空间的位置和大小由子类完成 — 实现顺序存储结构线性表的关键操作(增、删、查、等) — 提供数组操作符,方便快速获取元素SeqList.h #ifndef SEQLIST_H #define SEQLIST_H#include "…
暂无图片
编程学习 ·

spring注解式开发

一、@Configuration 声明一个类相当于配置类似于xml的配置文件,声明一个或者多个@Bean方法,并由spring容器管理,以便于在运行中为这些bean生成BeanDefinition和服务请求。 @Configuration //包扫描 @ComponentScan(value = "com.alibaba") }) public class MyConf…
暂无图片
编程学习 ·

Paddle_程序员必备的数学知识_转发

程序员——必备数学知识!!!Attention 本博客转发至百度aistudio的<深度学习7日入门-cv疫情检测>,课程非常棒!本人力推! 博客转发地址:https://aistudio.baidu.com/aistudio/projectdetail/604807 课程报名地址:https://aistudio.baidu.com/aistudio/education/group/i…
暂无图片
编程学习 ·

element dom 事件注册 on off once

/* istanbul ignore next */ // 匿名函数自执行,兼容IE-attachEvent,chrome-addEventListener export const on = (function() {if (!isServer && document.addEventListener) {return function(element, event, handler) {if (element && event && h…
暂无图片
编程学习 ·

Spring Boot / Spirng Cloud 引入Rabbit MQ

注意: spring cloud版本:Greenwich.RELEASE spring boot 版本: 2.1.5.RELEASE 1.导包,在pom.xml中导入<dependency><groupId>org.springframework.amqp</groupId><artifactId>spring-rabbit</artifactId></dependency>2.加入配置文件 …
暂无图片
编程学习 ·

Android视频的操作

上节课我们着重介绍了Android中的音频的处理,通过学习,我们已经熟悉并掌握了多媒体开发的几个操作,大致可以分为:a播放和采集编解码处理算法处理,实现特殊功能标准协议以及播放器工具类的开发 本节课我们来看一下Android的视频的相关操作。Android提供了常见的视频的编码、…
暂无图片
编程学习 ·

Mathmatica多项式带余除法代码

几乎没有调用内置函数,除了求多项式最高次数时用了一下 Exponent[] (*解析多项式*) (*将f=a0+a1*x+...+an*x^n解析成{{a0,0},{a1,1},...,{an,n}}的形式*) polyCoefficients[f_] := Module[{rules1 = {c_*base_^power_ -> {c, power},base_^power_ -> {1, power},c_*x_ -…
暂无图片
编程学习 ·

双亲委派模型

原理 双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加…
暂无图片
编程学习 ·

自定义注解,并通过注解进行数据库建表

1、自定义注解 1.1、这个注解用来指定表名 // An highlighted block package test.annotation;import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;@Target(…
暂无图片
编程学习 ·

抓头,0xc00005错误看到一个比较多的解释 记录一下

说明文字:PAGE-FAULT-IN-NONPAGED-AREA 通常的原因:内存错误(数据不能使用分页文件交换到磁盘中). 解决方法:卸掉所有的新近安装的硬件. 运行由计算机制造商提供的所有系统诊断软件.尤其是内存检查. 检查是否正确安装了所有新硬件或软件,如果这是一次全新安装,请与硬件或软…
暂无图片
编程学习 ·

HDFS架构

五.HDFS架构大多数分布式大数据框架都是主从架构HDFS也是主从架构Master|Slave或称为管理节点|工作节点主叫NameNode,中文称“名称节点”从叫DataNode,中文称“数据节点”5.1 NameNode5.1.1 文件系统file system文件系统:操作系统中负责管理文件、存储文件信息的软件具体地说…
暂无图片
编程学习 ·

docker常用简单命令

检查内核版本 uname -r 如果内核版本小于3.10执行 yum update 安装docker yum install docker 启动docker systemctl start docker 查看docker版本 docker -v 开机自启动docder systemctl enable docker 停止docker systemctl stop docker ///////////////////////////////////…
暂无图片
编程学习 ·

Centos7 配置FTP服务器

1.实验描述 注: vsftpd是一个较为安全的FTP服务器软件,本次使用vsftpd配置ftp服务器。 本次实验适用于新手入门学习 ①实验内容 1)开放实体用户登录 使用者登陆FTP的时候显示欢迎消息;系统帐号不允许登陆;允许实体用户进行上传、下载、建立目录及修改文件;设置用户新建…
暂无图片
编程学习 ·

ROS成长-wiki-ros教程整理 一

初学者学习ROS的一个很好的途径就是通过wiki教程来初步了解ros、安装ros、操作简单功能。了解后需要进阶学习可以参考各种开源项目,请教技术大牛博客大神等(可以参考古月居博客) 。 wiki是初学者跳不开的学习过程。学习过程中,哪管它前路坎坷,进一步就有一步的成长。http:…
暂无图片
编程学习 ·

Spring中MultipartHttpServletRequest实现文件上传

实现图片上传 用户必须能够上传图片,因此需要文件上传的功能。比较常见的文件上传组件有Commons FileUpload(http://jakarta.apache.org/commons/fileupload/a>)和COS FileUpload(http://www.servlets.com/cos),Spring已经完全集成了这两种组件,这里我们选择Commons …