并发编程专题五-AbstractQueuedSynchronizer源码分析
PS:外号鸽子王不是白来的,鸽了好几天,也是因为比较忙,时间太少了,这篇东西有点多,需要慢慢消化。不知不觉居然写了4个多小时....
一、什么是AQS
aqs是AbstractQueuedSynchronizer的简称,是用来构建锁或者其他同步组件(信号量、事件等)的基础框架类。JDK中许多并发工具类的内部实现都依赖于AQS,如ReentrantLock, Semaphore, CountDownLatch等等。
二、AQS的设计模式
2.1模板方法设计模式
在学习原理和源码之前,我们先了解一中设计模式。模板方法设计模式。
模板方法设计模式定义一个操作中算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变算法的结构即可重定义该算法的某些特定步骤。
通俗来说就是完成一件事情,有固定的数个步骤,但是每个步骤根据对象的不同,而实现细节不同;就可以在父类中定义一个完成该事情的总方法,按照完成事件需要的步骤去调用其每个步骤的实现方法。每个步骤的具体实现,由子类完成。
例如 发短信有以下有以下四个步骤:
1,需要发送给的人; 2 编写内容; 3,发送日期 4, 调用短信网关发送
发邮件也同样有以下四个步骤:
1,需要发送给的人; 2 编写内容; ,发送日期 4, 调用邮箱服务发送
这时,可以清楚地发现,无论是发短信还是发邮件,它们的步骤几乎是相同的: 1, 需要发送给的人; 2 发送内容 3,发送日期 4, 发送。只有具体到发送方法的时候,它们有些步骤才不同。这时我们就可以把相同的步骤提取出来,生成一个模版,进行共用,而到具体的内容时,它们再有具本的实现。 具体到代码时,模版就可以用抽象类实现,具体的内容可以到它的子类中实现。
代码如下
import java.util.Date; /** * @Auther: DarkKing * @Date: 2019/4/21 12:09 * @Description: */ public abstract class SendTemplate { //发送给谁,具体发送给谁需要子类去实现 public abstract void toUser(); //发送内容,具体发送内容需要子类去实现 public abstract void content(); //发送日期,因为日期都是一样的,所以父类就可以实现掉 public void date() { System.out.println(new Date()); } //发送方法,不同的发送方式肯定要实现不同的发送方法 public abstract void send(); //发送消息,框架方法-模板方法 public void sendMessage() { toUser(); content(); date(); send(); } } /** * @Auther: DarkKing * @Date: 2019/4/21 12:09 * @Description: 短信发送模板实现类 */ public class SendSms extends SendTemplate { @Override public void toUser() { System.out.println("to Pistachio"); } @Override public void content() { System.out.println(" I LOVE YOU ❤"); } @Override public void send() { System.out.println("set sms"); } public static void main(String[] args) { SendTemplate sendSms = new SendSms(); sendSms.sendMessage(); } }
AQS就是采用了模板方法的设计模式,除了AQS,Spring加载配置的过程同样也是使用了这种设计模式。具体以后Spring源码专题在给大家详细说明
2.2AQS中的方法
既然知道AQS所使用的是模板方法设计模式,那具体都有哪些方法,我们现在列举一下。
1、模板方法
独占式(独占锁) | 共享式(共享锁) | 方法描述 |
acquire | acquireShared | 获取锁方法,获取同步状态 |
acquireInterruptibly | acquireSharedInterruptibly | 获取锁方法,同acquire,但可以响应中断 |
tryAcquireNanos | tryAcquireSharedNanos | 获取锁方法 |
release | releaseShared | 释放锁 |
2、需要子类覆盖的流程方法
独占式获取 tryAcquire
独占式释放 tryRelease
共享式获取 tryAcquireShared
共享式释放 tryReleaseShared
这些方法我们可以看到,AQS中只抛出了一个UnsupportedOperationException异常,所以需要我们在子类中具体去实现。
3、其他方法
isHeldExclusively:判断同步器是否处于独占模式
除此之外,AQS还定义了一个volatile变量state,用于记录锁的状态
getState:获取当前的同步状态
setState:设置当前同步状态
compareAndSetState:使用CAS设置状态,保证状态设置的原子性
为什么Doug Lea大师要这样设计AQS呢?AQS面向的是锁的实现者,而我们在使用Lock锁的时候,只需要调用Lock对应的方法即可,屏蔽了实现细节。而对于锁的实现者来说,简化了锁的实现方式,例如同步状态管理,线程排队等底层操作。隔离了锁的实现者和锁的使用者。从而进行了解耦,当我们在使用ReentrantLock等锁的时候,完全感觉不到AQS的存在。
2.3自定义锁的实现
既然我们了解了AQS中的一些方法,那我们就通过实现父类中的方法,来自己实现一个锁,加深下对模板方法的理解。
自己实现锁的话,首先我们实现Lock接口,Lock共有6个接口,之前我们都已经讲过了,大家如果感兴趣,我的并发编程专题里查看哈~
import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.AbstractQueuedSynchronizer; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; /** * @Auther: DarkKing * @Date: 2019/4/21 12:09 * @Description: */ public class SelfLock implements Lock{ //aqs中state 表示获取到锁的状态 state=1 获取到了锁,state=0,表示这个锁当前没有线程拿到 //定义一个内部类,实现AQS模板方法 private static class Sync extends AbstractQueuedSynchronizer{ //是否占用 protected boolean isHeldExclusively() { return getState()==1; } //尝试获取锁 protected boolean tryAcquire(int arg) { //CAS操作,首先对比锁是否被获取到,获取到的话就将锁的状态置为1,否则获取锁失败 if(compareAndSetState(0,1)) { //设置锁的拥有者为当前线程 setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } //尝试释放锁 protected boolean tryRelease(int arg) { //如果锁的状态为0,则不需要释放,抛出异常 if(getState()==0) { throw new UnsupportedOperationException(); } //设置锁的拥有者为null,并且状态设置为0 setExclusiveOwnerThread(null); setState(0); return true; } //Condition对象,用于对锁的对象唤醒和等待 Condition newCondition() { return new ConditionObject(); } } private final Sync sycn = new Sync(); //获取锁。如果锁已被其他线程获取,则进行等待 @Override public void lock() { sycn.acquire(1); } //可以直接调用父类的方法。该方法和lock的区别就是可以响应中断。 @Override public void lockInterruptibly() throws InterruptedException { sycn.acquireInterruptibly(1); } //它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败返回false @Override public boolean tryLock() { return sycn.tryAcquire(1); } //带时间戳的获取锁,如果等待时间超过,则获取失败 @Override public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { return sycn.tryAcquireNanos(1, unit.toNanos(time)); } //释放锁 @Override public void unlock() { sycn.release(1); } //返回Condition对象 @Override public Condition newCondition() { return sycn.newCondition(); } }
以上方法我们实现了获取锁和释放锁的接口。从实现中我们发现获取锁的时候使用了CAS操作,但释放锁的时候没有进行CAS操作。这样写会不会出现问题呢?接下来写个测试类。测试下锁能否正常使用
import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import com.xiangxue.tools.SleepTools; /** * @Auther: DarkKing * @Date: 2019/4/21 12:09 * @Description: */ public class TestMyLock { public void test() { final Lock lock = new SelfLock(); class Worker extends Thread { public void run() { while (true) { lock.lock(); try { SleepTools.second(1); System.out.println(Thread.currentThread().getName()); SleepTools.second(1); } finally { lock.unlock(); } SleepTools.second(2); } } } // 启动10个子线程 for (int i = 0; i < 10; i++) { Worker w = new Worker(); w.setDaemon(true); w.start(); } // 主线程每隔1秒换行 for (int i = 0; i < 10; i++) { SleepTools.second(1); System.out.println(); } } public static void main(String[] args) { TestMyLock testMyLock = new TestMyLock(); testMyLock.test(); } }
我们定义了10个线程,打印出当前获取锁的线程。从打印我们可以看出来,每次只会打印出一个线程名。说明我们写的锁是起作用的。那为什么释放的时候不需要CAS操作呢。当我们一个线程获取到锁,其他线程又都在做什么呢。接下来我们走进AQS源码中,进行学习。
三、AQS源码解析
我们上面的测试用例中。当我们一个线程获取到锁,其他线程再去获取锁的时候,我们返回了false,线程进入的等待状态,那既然线程进入了等待状态,等待锁被释放的时候了进行唤醒。那么必然要有个地方存储我们进行等待的线程的一个数据结构。
3.1、AQS中的数据结构-节点和同步队列
AQS的中的等待的线程全部存储在一个同步队列里,它是一个先进先出的数据结构,先进来的线程会先被唤醒。同时这个队列还是个双向列表。上一个节点指向下一个节点。还有头指示器指向第一个节点。尾指示器指向最后一个节点。
3.2 同步队列节点属性
打开AQS的源码,找到Node类,所有属性如下
字段名 | 属性值 | 描述 |
CANCELLED | 1 | 线程等待超时或者被中断了,需要从队列中移走 |
SIGNAL | -1 | 后续的节点等待状态,当前节点如果完成则通知后面的节点去运行 |
CONDITION | -2 | 当前节点处于等待队列 |
PROPAGATE | -3 | 共享,表示状态要往后面的节点传播 |
0 | 初始状态 | |
waitStatus | 标识当前节点状态,就是上面的那几个常量字段。 | |
Node prev | 表示当前节点的前驱节点 | |
Node next | 表示当前节点的后驱节点 | |
Thread thread | 表示当前节点存放的线程 | |
Node nextWaiter | 指向等待队列节点 |
3.3 获取锁的过程
当我们执行获取acquire锁的方法时
如果获取失败,则会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,该方法首先会执行addWaiter(Node.EXCLUSIVE), arg),
该方法是将线程包装成Node节点,通过CAS操作,将节点加入到同步队列中。
尾节点变化如图所示
因为多线程的原因,为了保持原子性,所以这里需要使用CAS操作。不然可能会出现尾部节点数据丢失等问题。如果CAS对比操作失败,则执行enq方法,将该操作放入循环中。直到添加成功。(CAS的基本操作)
当Node已经放入了同步队列之后,那么这个线程说明需要进行等待。排队排到我的时候我要去获取锁。所以acquireQueued方法中,会一直循环获取锁。
首先获取前置节点,如果前置节点是头节点,则直接去尝试获取锁。当获取锁成功之后,将该节点设置为头节点,之前的头结点设置为null,脱钩帮助GC。然后returen false;说明获取锁成功。不需要在进行等待。
如果获取锁失败,则会执行parkAndCheckInterrupt()方法,将自己阻塞。这是整个获取锁的一个过程。
3.4 释放锁的过程
释放锁的方法为我们定义的unlock();实际调用的AQS中的release方法。
该方法首先执行我们实现的TryRelease尝试释放锁,如果锁释放成功,则获取头结点,然后执行unparkSuccessor(h)方法。
这个方法主要就是获取头结点的下一个节点,如果下个节点为null,则将下个节点的前一个节点设置为尾部,否则的话然后进行唤醒。
首节点的变化如图所示
当一个节点释放锁之后,修改头结点时同时只会有一个线程去操作,所以不需要CAS操作,直接将头节点设为null,然后修改同步器头部指向下一个节点。
整体获取和释放锁的过程如下所示
3.5、AQS中的数据结构-节点和等待队列
每一个锁都有一个Condition对象,该对象主要实现await和signal等方法。用于线程的等待和唤醒。因此每个Condition对象中肯定也存在一个等待队列。
等待队列的数据结构
等待队列和同步队列的区别
1、等待队列里的节点和同步队列节点都是同一个属性。唯一的不同就是等待队列是一个单向链表,而非双向链表。
2、一个同步器(锁)里面,只会有一个同步队列,但可以有多个等待队列。如下图所示。
一个锁可以创建个多个Condition,每一个Condition下都会有一个等待队列。
private Lock lock = new ReentrantLock(); private Condition keCond = lock.newCondition(); private Condition siteCond = lock.newCondition();
3.6、await方法过程
await表示使当前线程进入等待状态。
首先调用addConditionWaiter方法将该线程包装成Node加入到阻塞队列中去,然后调用fullyRelease方法,将该线程所持有的锁进行释放。在while中判断线程是否被唤醒,如果没有,则进行阻塞。
await方法过程
3.7、single方法过程
single唤醒等待的一个线程。
当执行single的时候,首先需要从等待队列中取出一个节点,如果不为null,则执行doSignal方法。
如果等待队列的下一个节点为null,则把末尾节点设为null
然后调用transferForSignal方法,判断当前节点的状态,如果不能修改值,则取消,如果修改成功,则嗲用enq。将该节点移动到同步队列尾部,并设置waitStatus为SIGNAL。满足条件后唤醒线程。
single流程图如下
因为每个condition对象都会有一个同步机制,而且调用single会指定唤醒对应等待队列的线程,不会丢失信息。所以建议使用single方法唤醒,而不是调用singleAll,而且每次调用singleAll还要将所有等待队列的节点全部移动到同步队列中。
大体上学完AQS,要了解模板方法设计,会自己手动实现锁,了解获取锁和释放锁,以及基于锁的等待和唤醒机制。大家如果还有什么问题,可以加我微信一起讨论哈。
其他阅读 并发编程专题
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
死磕 java集合之PriorityQueue源码分析
问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合,集合中的每个元素都有一个权重值,每次出队都弹出优先级最大或最小的元素。 一般来说,优先级队列使用堆来实现。 还记得堆的相关知识吗?链接直达【拜托,面试别再问我堆(排序)了!】。 那么Java里面是如何通过“堆”这个数据结构来实现优先级队列的呢? 让我们一起来学习吧。 源码分析 主要属性 // 默认容量 private static final int DEFAULT_INITIAL_CAPACITY = 11; // 存储元素的地方 transient Object[] queue; // non-private to simplify nested class access // 元素个数 private int size = 0; // 比较器 private final Comparator<? super E> comparator; // 修改次数 transien...
- 下一篇
Spring Cloud Alibaba基础教程:Sentinel使用Apollo存储规则
上一篇我们介绍了如何通过Nacos的配置功能来存储限流规则。Apollo是国内用户非常多的配置中心,所以,今天我们继续说说Spring Cloud Alibaba Sentinel中如何将流控规则存储在Apollo中。 使用Apollo存储限流规则 Sentinel自身就支持了多种不同的数据源来持久化规则配置,目前包括以下几种方式: 文件配置 Nacos配置 ZooKeeper配置 Apollo配置 本文我们就来一起动手尝试一下,如何使用Apollo来存储限流规则。 准备工作 下面我们将同时使用到Apollo和Sentinel Dashboard,所以可以先把Apollo和Sentinel Dashboard启动起来。 如果还没入门Sentinel Dashboard可以通过文末的系列目录先学习之前的内容。Apollo的话相对复杂一些,这里不做详细介绍了,如果还没有接触过Apollo的读者可以查看其官方文档进一步学习。 应用配置 第一步:在Spring Cloud应用的pom.xml中引入Spring Cloud Alibaba的Sentinel模块和Apollo存储扩展: <d...
相关文章
文章评论
共有0条评论来说两句吧...