并发编程之AQS(抽象队列同步器)独占锁
前言
在介绍AQS之前,我专门写了CAS、Volatile、synchronized、阻塞队列,链表以及线程中断的笔记,因为在学习AQS之前,如果对于这些基础的东西不了解的话,那么AQS你是看不懂的,AQS我自己认为还是比较难的,但是只要是学习技术,那么如果都那么简单的话,那也就没有专门来学习的必要了,我也难得写笔记来记录它了,因为如果简单,就想1+1=2,没有必要来记录,来记录的目的也是对自己这么多年工作所得的经验的一个记录,不管写的怎么样,既然写出来和大家分享,那么可能写的不是那么好,但是你要是喜欢就看,不喜欢就可以退出,不喜勿喷。我只是将自己这么多年的学习的技术栈记录下来,我本人很喜欢研究底层,所以大多数都是我底层研究所得,不会为了写一个技术去网络上各种搜索,说实话,与其去网络上搜索来记录,还不如自己去研究底层,因为底层的实现原理不会骗人,你自己研究懂了,你就是最专业的,同一个技术原理,可能网络上五花八门,把你都绕晕了,你都不知道该信谁的,要是你自己去研究底层所得,那么它就是对的,你就是专业的,所以写笔记或者博客也只是为了记录自己的所得,也可以作为生活的轨迹,人生轨迹的一部分吧;好了,今天废话有点多,下面开始今天的主题,AQS同步器的记录。
AQS原理
Java并发编程的核心在于java.util.concurrent包。而juc当中大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这个行为的抽象就是基于AbstractQueuedSynchronizer,简称AQS。AQS定义了一套多线程访问共享资源的同步器框架,是一个依赖状态(state)的同步器
AQS具备特性:
阻塞等待队列
共享/独占
公平/非公平
可重入
允许中断
这些特性是怎么实现的,以ReentrantLock为例:
一般通过定义内部类Sync [sɪŋk] 继承AQS
将同步器所有调用都映射到Sync对应的方法
同步器队列结构
同步器实现的数据结构为双向链表结构,我们先来了解下双向链表结构,比如下图:
双向链表的几个基本元素:
head:头结点(指向第一个节点)
tail:尾结点(指向最后一个节点)
prev:前驱节点
nex:后驱节点
双向链表结构在在内存中不是连续的,可能分布在内存的不同区域cell中,它们是通过指针引用关系进行联系的;
ReentrantLock
ReentrantLock是可重入,公平,非公平,可独占锁,它和synchronized的区别这里就不说了,前几篇文章已经说得够多了,这里先看一段程序代码:
public class AqsTest {
private final static Lock lock = new ReentrantLock();
static int sum = 0;
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= 3; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
lock.lock();
try {
for (int j = 1; j <= 10000; j++) {
sum ++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}, "Thread-" + i);
thread.start();
}
Thread.sleep(5000);
System.out.println(sum);
}
}
上面的程序代码中启动而来3个线程,每个线程给sum+=10000,如果是线程安全的,那么最后sum的结果肯定是30000,如果我们去掉lock,那么肯定没有办法输出正确结果的,所以lock和synchronized想要达到的目的是一样的,就看在什么场景下使用,在jdk的底层代码阶段,synchronized、lock、cas、Unsafe用的非常多,可以说是开发jdk api的基础,通过上面的例子来分析下ReentrantLock
类构造
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
/**
* Base of synchronization control for this lock. Subclassed
* into fair and nonfair versions below. Uses AQS state to
* represent the number of holds on the lock.
*/
abstract static class Sync extends AbstractQueuedSynchronizer {
ReentrantLock 实现了Lock接口,Lock接口中定义了锁的基本操作接口方法,比如我们自己要实现一个自己的锁,那么我们也可以实现Lock接口即可;然后ReentrantLock 中有个属性Sync,Sync是继承AbstractQueuedSynchronizer (AQS)抽象队列同步器,而AQS中定义了很多同步队列操作方法,为什么说AQS是抽象队列同步器,因为AQS中定义了很多同步操作方法,包括数据队列双向链表结构,以及很多同步操作的方法,是一个标准;简单来说就是AQS采用了模板设计方法的设计模式来设计AQS,后面的同步器都是基于它的标准来的。
我这边基于3个线程来把ReentrantLock的lock和unlock分析下,首先看构造方法,我们知道ReentrantLock是可公平,非公平的,就是你在构造的时候如果传入true就是公平锁,如果不传入fair或者传入的false就是非公平锁,非公平锁意思就是插队,公平的话就每次进入队列,不能进行插队,我们看下构造:
//下面这个构造就是非公平锁的实现
//非公平的锁是由NonfairSync实现的
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
//下面这个构造可以构造公平或者非公平,根据你传入的
//fair是否为true,公平锁是由FairSync来实现的
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
//公平锁直接放入队列
final void lock() {
acquire(1);
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
//非公平锁,lock的时候下面的cas和setExclusiveOwnerThread就是
//插队
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
队列的数据结构信息Node
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;
/**
* 节点在等待队列中,节点的线程等待在Condition上,当其他线程对Condition调用了signal()方法后,
* 该节点会从等待队列中转移到同步队列中,加入到同步状态的获取中
*/
static final int CONDITION = -2;
/**
* 表示下一次共享方式同步状态获取将会被无条件的传播下去
*/
static final int PROPAGATE = -3;
/**
* 标记当前节点的信号量状态(1,0,-1,-2,-3)5种状态
* 使用CAS更改状态,volatile保证线程可见性,并发场景下,
* 即被一个线程修改后,状态会立马让其他线程可见
*/
volatile int waitStatus;
/**
* 前驱节点,当前节点加入到同步队列中被设置
*/
volatile Node prev;
/**
* 后继节点
*/
volatile Node next;
/**
* 节点同步状态的线程
*/
volatile Thread thread;
/**
* 等待队列中的后继节点,如果当前节点是共享的,那么这个字段是一个SHARED常量
* 也就是说节点类型(独占和共享)和等待队列中的后继节点公用一个字段
* (用在条件队列里面)
*/
Node nextWaiter;
}
这个笔记目前只介绍独占锁,但是 AQS是有共享锁的,共享锁在下一个笔记中记录
lock过程
我也在想如何写这个笔记,才能让之后的自己或者别人能够很快理解AQS独占锁的原理,我这边通过程序和debug来分析,在idea中设置debug的时候看线程:
这样设置了,启动了程序过后,就可以看指定的线程
我这边只有3个线程,所以就可以debug切换看线程,因为循环是从1开始的,所以很肯定线程1先启动,我们看下:
线程1执行到这里,开始上锁,因为我设置的锁是非公平锁,所以这里进入的非公平锁,
我们分析下代码:
final void lock() {
//线程的有一个state非常重要,因为ReentrantLock是可重入的锁
//所以这里cas将state由0修改为1,这里肯定是能成功的,因为
//目前就一个线程到这里,所以它肯定能成功,这个时候state=1
if (compareAndSetState(0, 1))
//cas成功过后,这里是独占锁,独占锁,exclusiveOwnerThread
//表示独占这个线程,也就是说执行了下面的代码,当前只有这个线程
//可以运行,而Thread.currentThread()目前是Thread-1
setExclusiveOwnerThread(Thread.currentThread());
else
//这里Thread-1肯定就是不能进入的,这里的意思是如果cas失败,cas失败
//的情况就是当前有线程获取了锁,我没法获取所,我就进入队列中
acquire(1);
}
现在线程1获取了锁,就可以执行自己的业务逻辑了,现在我们切换到Threa-2,看下
看上图,此时,Threa-1获取了锁,正在执行,这个时候Thread-2又来获取所,很显然,Thread-2是没有办法获取锁的,所以进入队列,我们来看下acquire方法
//arg传入的是1
public final void acquire(int arg) {
//这个if分为了2部分,第一部分是尝试下获取所,万一
//这个时候Thread-1释放了,但是我这边是用debug,Thread-1还堵塞
//在,所以第一个条件肯定是false
//第二个条件中的括号中的是添加到waiter,也就是添加到等待队列
if (!tryAcquire(arg) &&
//这个Node.EXCLUSIVE表示锁的类型,这个是独占模式,目前是null
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个添加到等待队列的deubg如上,首先来创建一个节点,这个节点要存放我们的添加的线程的信息,这个时候
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;//null
this.thread = thread;//Thread-1
}
//添加Thread-2到等待队列
private Node addWaiter(Node mode) {
//构建一个节点,用来存放Threa-2,mode=null
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
//声明一个pred节点,将尾结点赋值给pred,但是Thread-2进入到这里
//Threa-1已经在运行,不在队列,所以这里Thread-2到这里的时候
//tail肯定为空
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//进队
enq(node);
return node;
}
这个时候enq进队,上面创建的一个节点node,看下截图,这个时候前驱,后驱都是空的,因为这个队列刚创建了一个节点
private Node enq(final Node node) {
for (;;) {
//首先把尾结点给Node t,这个时候|Thread-2到这里时,
//看上图就知道这个时候head、tail都是空的,所以这里进入if
Node t = tail;
if (t == null) { // Must initialize
//创建一个空节点,将空节点通过cas设置到head,也就是创建的这个
//节点是头节点head,因为这个时候在构建整个队列
//如果只创建一个节点,所以这个时候head=tail
//也就是说head也是tail,tail也是head
if (compareAndSetHead(new Node()))
tail = head;
} else {
//因为这里是一个循环,所以第二次循环肯定能到这里
//第一次循环已经创建了头节点和尾结点了,
//这个时候t是指向尾结点的
//所以这里构建队列的结构的时候,node是上面构建的一个
//Threa-2的节点,这里将尾结点tail指向node当前前驱节点
//什么意思呢,就是尾结点发送变化,尾结点为node
//tail.prev = node
node.prev = t;
//将尾结点设置为node
if (compareAndSetTail(t, node)) {
//cas成功过后,整个队列就构建成功了
//head=new Node()
//tail=node
//node.prev=t
//t.next=node
t.next = node;
return t;
}
}
}
}
enq执行完成过后到这里,这里其实就是阻塞线程的
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//Thread-2到这里node是Threa2,这里获取
//的p就是node的前驱节点,这里判断前驱节点是否
//是head节点和尝试获取锁,这个时候获取锁
//失败的
final Node p = node.predecessor();
//获取锁过后,设置当前线程为Thead-2,但是这里不能成功的
//因为Thead-1还没有释放锁
if (p == head && tryAcquire(arg)) {
//获取锁过后,设置头节点的线程为空
//这里要记得,获取锁过后的前驱节点永远是空的
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//这个时候Thread-2的waitStatus=0,修改状态为-1,表示
//下次可以被唤醒
//parkAndCheckInterrupt是调用parak进行阻塞
//所以线程2在这里就被阻塞在这里,等待下次唤醒
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
//这个方法就是修改waitStatus,-1表示可以被唤醒启动
//如果waitStatus=0,则修改为-1,下次可启动
//如果waitStatus=-1表示可以启动
//如果waitStatus > 0表示线程无效的,要剔除
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 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.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
切换线程3,debug如下:
这个时候tail不为空,所以进入if,cas修改尾结点为新创建的节点
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
//Threa-3进入这里,新创建的节点的前驱节点为尾结点
//就是入队的操作,原来的尾结点往前移动,新创建的节点作为
//尾结点,然后把新创建的节点通过cas修改为尾结点
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
然后Threa3和线程2一样的调用acquireQueued进行阻塞,这个过程中我也不知道如何表述才能完全的表述出来,大概的意思就是同时3个线程进入lock的时候,只有一个lock能进入执行,其他两个线程都进入了等待队列阻塞,然后线程1执行完成过后,调用unlock解锁过后,线程2和线程3才能去获取锁,然后执行完解锁,后面的线程才能获取锁,我们先通过图来理解下过程,lock完了之后再进行unlock的解读,我先来看下获取锁的过程
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//state =0才能进行尝试获取锁,每次获取锁过后+1
int c = getState();
if (c == 0) {
//cas修改state=1
if (compareAndSetState(0, acquires)) {
//修改成功设置当前线程为独占线程
setExclusiveOwnerThread(current);
return true;
}
}
//下面这个判断是如果已经获取了锁,再次获取锁,也就是
//重复获取锁,因为lock是可以重入的,所以下面的state+1
//直到state=0才会释放锁
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;
}
获取锁的逻辑非常简单,就是一般的逻辑代码
lock图解过程
如上图所示,循环启动3个线程,每个线程循环1万次,上图的过程只是每个线程lock的过程,其中只有第一个线程能够获取锁即Thread-1,Thread-2和Thread-3都是先入队,然后进行各自阻塞,通过上面的分析可知,每次获取不到锁过后,在阻塞之前,都会回去前一个节点然后修改前一个节点的waitStatus=-1,-1表示可以被唤醒的意思;文字描述下3个线程lock的过程:
Thread-1:线程1进入lock时,这个时候没有其他线程在运行,它肯定是能够获取到锁的,所以cas是能够成功的,cas修改的是state由0修改为1,state是控制线程是否能够运行的前置条件,cas成功,设置独占锁为当前线程;
Thread-2:线程2进来就比较悲催了,因为Thread-1获取了锁,那么线程2还是需要尝试去获取锁,也是想把state由0修改为1,但是这个时候cas失败的,因为Thread-1把state已经修改为1了,cas肯定失败,所以失败了就入队,这个时候队列还是空的,所以分两步:
1.创建Node节点对象node,Node属性为:
waitStatus=0
thread=Thread-2
nextWait=null
pre=null
1.创建一个队列的空节点t,这个节点作为队头head和队尾tail,Node属性为
waitStatus=0
thread=null
nextWait=null
pre=null
2.第二次循环将node节点通过cas修改为尾结点,然后node.prev=t,t.next=node;
3.将线程2节点添加的队列中过后,然后进行获取node.prev节点,修改prev的节点的waitStatus由0修改为-1,表示可以被唤醒的线程;
Thread-3:线程3一样的进行lock的时候也要尝试cas,很显然cas也是失败的,和线程2一样去添加到等待队列中,这个时候:
head node
waitStatus=-1
thread=null
nextWait=Thread-1
pre=null
thread-1 node:
waitStatus=-1
thread=Thread
nextWait=thread2 node
pre=head node
thread-2 node(tail):
waitStatus=0
thread=Thread-2
nextWait=null
pre=thread-1 node
最后就完成了这种双向链表的数据结构
unlock过程
通过上面的debug过程过后,我们的3个线程目前是这样的状况:
Thread-1已经执行完了自己的业务逻辑,准备解锁了
而线程2和3都是出于这种状况,就是线程被挂起的状态,2和3已经被阻塞挂起了,我们继续debug让Thread-1解锁
线程1解锁debug进入这里,我先来分析写这个方法的业务逻辑
final boolean release(int arg) {
//tryRelease解锁的过程
if (tryRelease(arg)) {
//解锁成功过后,从对头开始唤醒下一个线程开始执行
//也就是说每一个线程在结束过后会自动去唤醒下一个线程开始执行
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//线程解锁的过程,需要将state-1
int c = getState() - releases;
//如果要解锁的线程和目前独占的线程不一样,则知报错,
//报的非法monitor状态异常,我们还记得吗?
//我们直接在非synchronized的代码块中使用wait也会报这个错误
//所以这里的判断就是你解锁的线程根本不是独占的线程
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
//将独占线程设置为null,下一个获取锁的线程就可以设置当前线程
setExclusiveOwnerThread(null);
}
//将state回写到内存,让所有的线程都可以看到
setState(c);
return free;
}
//向被阻塞的线程发一个许可unpark,告诉它可以执行了
//node=head节点,然后唤醒的目标是node.next
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
//把node=head的头节点的waitstatus修改为0
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
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);
}
我们线程1唤醒线程2过后,其实s.thread就是线程2,我们再切到线程2看下线程2在哪里
可以看到线程2还在原来的哪里阻塞到再,therad-1唤醒它过后,它开始执行了,现在线程2要获取锁是能获取成功了
线程2执行tryAcquire已经成功了,首先node.prev肯定是等于head的,因为如果从队列中唤醒的线程的节点永远是第二个节点,也就是head.next=node,所以这里会进去
我们着重分析下if的代码块
//下面的代码其实简单来说就是出队,将头节点出队
//设置当前执行的节点为头结点
setHead(node);
//节点往前移动了,原来的节点进行回收,方便gc
p.next = null; // help GC
failed = false;
return interrupted;
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
执行完上述代码过后,节点信息如下:
head(del head head = node1):
waitStatus=0
thread=null
prev=null
next=node2
node1(del node1 ):没有了,node1就是当前运行的线程1
node2:
waitStatus=0
thread=thread-3
prev=head
next=null
这就是整个解锁的过程,是配合加锁一起完成的,原理就是每个获得lock的线程在完成过后解锁完成过后,要讲队列中head.next的节点进行出队执行,所以aqs独占锁有个特点
就是头结点head用元是空节点,在构建节点的时候第一次创建的也是空节点,所以head节点我觉得就是一个过渡,为了得到next节点的信息的一个数据结构;
还有就是有两个重要的信息需要注意:
state:线程获取锁过后+1,每个线程需要获取锁的条件是state=0
waitStatus:线程被唤醒的状态,-1才能阻塞被唤醒,但是不包括最后一个线程
因为每次入队更新的时候都是prev更新为-1,那么最后一个节点的线程永远是0;
unlock图解过程
unlock的解锁过程如上图,简单来说就是执行完成过后,释放锁,然后从队列中取第一个节点然后唤醒它可以出队执行了,然后唤醒的节点开始获取锁,获取锁成功过后出队执行就是这么个过程,过程简单,主要是吸收Doug Lea大神的思想,别人为什么要这么做;这其中涉的两个参数state和WaitStatus和和加锁的一样。
总结
AQS独占锁这个原理,我有时候也不知道如何去写,自己懂和表述是两个概念,因为Doug Lea在实现的时候写的非常绕,首先要理解它的设计理念以及原理,否则代码能力再高的人也看不懂,懂了理念和设计原理再看就很轻松了,所以我就采用debug的模式截图和源码解释来解读AQS底层实现原理,如果谁看到了这篇文章,如果有疑问的大家可以进行交流。