死磕 java同步系列之AQS起篇
问题
(1)AQS是什么?
(2)AQS的定位?
(3)AQS的实现原理?
(4)基于AQS实现自己的锁?
简介
AQS的全称是AbstractQueuedSynchronizer,它的定位是为Java中几乎所有的锁和同步器提供一个基础框架。
AQS是基于FIFO的队列实现的,并且内部维护了一个状态变量state,通过原子更新这个状态变量state即可以实现加锁解锁操作。
本章及后续章节的内容理解起来可能会比较晦涩,建议先阅读彤哥上一章的内容【死磕 java同步系列之自己动手写一个锁Lock】。
核心源码
主要内部类
static final class Node {
// 标识一个节点是共享模式
static final Node SHARED = new Node();
// 标识一个节点是互斥模式
static final Node EXCLUSIVE = null;
// 标识线程已取消
static final int CANCELLED = 1;
// 标识后继节点需要唤醒
static final int SIGNAL = -1;
// 标识线程等待在一个条件上
static final int CONDITION = -2;
// 标识后面的共享锁需要无条件的传播(共享锁需要连续唤醒读的线程)
static final int PROPAGATE = -3;
// 当前节点保存的线程对应的等待状态
volatile int waitStatus;
// 前一个节点
volatile Node prev;
// 后一个节点
volatile Node next;
// 当前节点保存的线程
volatile Thread thread;
// 下一个等待在条件上的节点(Condition锁时使用)
Node nextWaiter;
// 是否是共享模式
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(Thread thread, Node mode) { // Used by addWaiter
// 把共享模式还是互斥模式存储到nextWaiter这个字段里面了
this.nextWaiter = mode;
this.thread = thread;
}
// 节点的构造方法
Node(Thread thread, int waitStatus) { // Used by Condition
// 等待的状态,在Condition中使用
this.waitStatus = waitStatus;
this.thread = thread;
}
}
典型的双链表结构,节点中保存着当前线程、前一个节点、后一个节点以及线程的状态等信息。
主要属性
// 队列的头节点
private transient volatile Node head;
// 队列的尾节点
private transient volatile Node tail;
// 控制加锁解锁的状态变量
private volatile int state;
定义了一个状态变量和一个队列,状态变量用来控制加锁解锁,队列用来放置等待的线程。
注意,这几个变量都要使用volatile关键字来修饰,因为是在多线程环境下操作,要保证它们的值修改之后对其它线程立即可见。
这几个变量的修改是直接使用的Unsafe这个类来操作的:
// 获取Unsafe类的实例,注意这种方式仅限于jdk自己使用,普通用户是无法这样调用的
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 状态变量state的偏移量
private static final long stateOffset;
// 头节点的偏移量
private static final long headOffset;
// 尾节点的偏移量
private static final long tailOffset;
// 等待状态的偏移量(Node的属性)
private static final long waitStatusOffset;
// 下一个节点的偏移量(Node的属性)
private static final long nextOffset;
static {
try {
// 获取state的偏移量
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
// 获取head的偏移量
headOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
// 获取tail的偏移量
tailOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
// 获取waitStatus的偏移量
waitStatusOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("waitStatus"));
// 获取next的偏移量
nextOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("next"));
} catch (Exception ex) { throw new Error(ex); }
}
// 调用Unsafe的方法原子更新state
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
关于Unsafe类的讲解请参考彤哥之前写的【死磕 java魔法类之Unsafe解析】。
子类需要实现的主要方法
我们可以看到AQS的全称是AbstractQueuedSynchronizer,它本质上是一个抽象类,说明它本质上应该是需要子类来实现的,那么子类实现一个同步器需要实现哪些方法呢?
// 互斥模式下使用:尝试获取锁
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
// 互斥模式下使用:尝试释放锁
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
// 共享模式下使用:尝试获取锁
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
// 共享模式下使用:尝试释放锁
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
// 如果当前线程独占着锁,返回true
protected boolean isHeldExclusively() {
throw new UnsupportedOperationException();
}
问题:这几个方法为什么不直接定义成抽象方法呢?
因为子类只要实现这几个方法中的一部分就可以实现一个同步器了,所以不需要定义成抽象方法。
下面我们通过一个案例来介绍AQS中的部分方法。
基于AQS自己动手写一个锁
直接上代码:
public class MyLockBaseOnAqs {
// 定义一个同步器,实现AQS类
private static class Sync extends AbstractQueuedSynchronizer {
// 实现tryAcquire(acquires)方法
@Override
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 实现tryRelease(releases)方法
@Override
protected boolean tryRelease(int releases) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
}
// 声明同步器
private final Sync sync = new Sync();
// 加锁
public void lock() {
sync.acquire(1);
}
// 解锁
public void unlock() {
sync.release(1);
}
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
MyLockBaseOnAqs lock = new MyLockBaseOnAqs();
CountDownLatch countDownLatch = new CountDownLatch(1000);
IntStream.range(0, 1000).forEach(i -> new Thread(() -> {
lock.lock();
try {
IntStream.range(0, 10000).forEach(j -> {
count++;
});
} finally {
lock.unlock();
}
// System.out.println(Thread.currentThread().getName());
countDownLatch.countDown();
}, "tt-" + i).start());
countDownLatch.await();
System.out.println(count);
}
}
运行main()方法总是打印出10000000(一千万),说明这个锁也是可以直接使用的,当然这也是一个不可重入的锁。
是不是很简单,只需要简单地实现AQS的两个方法就完成了上一章彤哥自己动手实现的锁的功能。
它是怎么实现的呢?
我们这一章先不讲源码,后面学习了ReentrantLock自然就明白了。
总结
这一章就到此结束了,本篇没有去深入的解析AQS的源码,笔者认为这没有必要,因为对于从来都没有看过锁相关的源码的同学来说,一上来就讲AQS的源码肯定会一脸懵逼的,具体的源码我们穿插在后面的锁和同步器的部分来学习,等所有跟AQS相关的源码学习完毕了,再来一篇总结。
下面总结一下这一章的主要内容:
(1)AQS是Java中几乎所有锁和同步器的一个基础框架,这里说的是“几乎”,因为有极个别确实没有通过AQS来实现;
(2)AQS中维护了一个队列,这个队列使用双链表实现,用于保存等待锁排队的线程;
(3)AQS中维护了一个状态变量,控制这个状态变量就可以实现加锁解锁操作了;
(4)基于AQS自己动手写一个锁非常简单,只需要实现AQS的几个方法即可。
彩蛋
上一章彤哥自己动手写的锁,其实可以看成是AQS的一个缩影,看懂了那个基本上AQS可以看懂一半了,因为彤哥那个里面没有写Condition相关的内容,下一章ReentrantLock重入锁中我们将一起学习Condition相关的内容。
所以呢,还是建议大家去看看这篇文章,点击下面的推荐阅读可以直达。
推荐阅读
欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

死磕 java同步系列之AQS起篇的更多相关文章
- 死磕 java同步系列之AQS终篇(面试)
问题 (1)AQS的定位? (2)AQS的重要组成部分? (3)AQS运用的设计模式? (4)AQS的总体流程? 简介 AQS的全称是AbstractQueuedSynchronizer,它的定位是为 ...
- 死磕 java同步系列之CyclicBarrier源码解析——有图有真相
问题 (1)CyclicBarrier是什么? (2)CyclicBarrier具有什么特性? (3)CyclicBarrier与CountDownLatch的对比? 简介 CyclicBarrier ...
- 死磕 java同步系列之Phaser源码解析
问题 (1)Phaser是什么? (2)Phaser具有哪些特性? (3)Phaser相对于CyclicBarrier和CountDownLatch的优势? 简介 Phaser,翻译为阶段,它适用于这 ...
- 死磕 java同步系列之zookeeper分布式锁
问题 (1)zookeeper如何实现分布式锁? (2)zookeeper分布式锁有哪些优点? (3)zookeeper分布式锁有哪些缺点? 简介 zooKeeper是一个分布式的,开放源码的分布式应 ...
- 死磕 java同步系列之redis分布式锁进化史
问题 (1)redis如何实现分布式锁? (2)redis分布式锁有哪些优点? (3)redis分布式锁有哪些缺点? (4)redis实现分布式锁有没有现成的轮子可以使用? 简介 Redis(全称:R ...
- 死磕 java同步系列之终结篇
简介 同步系列到此就结束了,本篇文章对同步系列做一个总结. 脑图 下面是关于同步系列的一份脑图,列举了主要的知识点和问题点,看过本系列文章的同学可以根据脑图自行回顾所学的内容,也可以作为面试前的准备. ...
- 死磕 java同步系列之StampedLock源码解析
问题 (1)StampedLock是什么? (2)StampedLock具有什么特性? (3)StampedLock是否支持可重入? (4)StampedLock与ReentrantReadWrite ...
- 死磕 java同步系列之Semaphore源码解析
问题 (1)Semaphore是什么? (2)Semaphore具有哪些特性? (3)Semaphore通常使用在什么场景中? (4)Semaphore的许可次数是否可以动态增减? (5)Semaph ...
- 死磕 java同步系列之ReentrantReadWriteLock源码解析
问题 (1)读写锁是什么? (2)读写锁具有哪些特性? (3)ReentrantReadWriteLock是怎么实现读写锁的? (4)如何使用ReentrantReadWriteLock实现高效安全的 ...
随机推荐
- 【Linux命令】nohup和&差异,查看进程和终止进程!
最近在开发dueros的技能,官方提供的PHPSDK中有多个实力,而运行实例的命令如下是 nohup php -S 0.0.0.0:8029 myindex.php & 从命令来看,肯定是在8 ...
- MVC如何在解决方案下创建文件夹
背景:为什么要在解决方案下创建文件夹? 比如,在开发过程中,会抽象出大量的公共方法,如数据库访问的方法.配置文件读取方法等等,将这些方法生成自己的DLL库文件,方便在其他的项目中进行复用.那么,这些方 ...
- Python虚拟机之异常控制流(五)
Python中的异常控制语义结构 在Python虚拟机之异常控制流(四)这一章中,我们考察了Python的异常在虚拟机中的级别上是什么东西,抛出异常这个动作在虚拟机的级别上对应的行为,最后,我们还剖析 ...
- (手写)mybatis 核心配置文件和接口不在同一包下的解决方案
smart-sh-mybatis项目 app.xml文件中此处配置为: <!-- 从整合包里找,org.mybatis:mybatis-spring:1.2.4 --> <!-- s ...
- bzoj3262陌上花开 三维数点 cdq+树状数组
大早上的做了一道三维数点一道五位数点,神清气爽! 先给一维排序,变成一个奇怪的动态的二维数点(相当于有一个扫描面扫过去,导致一系列的加点和询问) 然后cdq分治,再变回静态,考虑前半段对后半段的影响 ...
- 你应该知道的.net平台下socket异步通讯(代码实例)
1,首先添加两个windows窗体项目,一个作为服务端server,一个作为客户端Client 2,然后添加服务端代码,添加命名空间,界面上添加TextBox控件 using System.Net; ...
- Java EE - Servlet 3.0 和 Spring MVC
Table of Contents 前言 基于 Java 的配置 ServletContainerInitializer 动态配置 DispatcherServlet 和 ContextLoaderL ...
- python - 接口自动化 - 接口测试基础知识
# -*- coding:utf-8 -*- '''@project: jiaxy@author: Jimmy@file: study_接口测试基础知识一.py@ide: PyCharm Commun ...
- Welcome-to-Swift-16自动引用计数(Automatic Reference Counting)
Swift使用自动引用计数(ARC)来跟踪并管理应用使用的内存.大部分情况下,这意味着在Swift语言中,内存管理"仍然工作",不需要自己去考虑内存管理的事情.当实例不再被使用时, ...
- 【Luogu】P1251餐巾计划(上下界费用流)
题目链接 学了一下上下界费用流,似乎很nb.但是我说得不好,所以这里给出博客链接. 某dalao的博客 然后这道题的解法就是先用上下界费用流的建图方式连早上和晚上之间的那条边,保证当天一定会有r条或以 ...