死磕abstractqueuedsynchronizer源码
第一次写博客,先练练手。
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类中内存中相对于该类首地址的偏移量。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
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)当前线程是属于重入,则增加重入次数
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中的链表结构图:
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); } }
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; }
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释放锁了,那么我们来看锁被释放后会产生什么效果
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; }
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;//获得 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方法中,所以被唤醒以后继续从这个方法开始执行。