前言
`
AQS 的全称为 AbstractQueuedSynchronizer,即抽象队列同步器。这个类在 java.util.concurrent.locks 包下面
AQS 就是一个抽象类,主要用来构建锁和同步器
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable
AQS 为构建锁和同步器提供了一些通用功能的是实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue等等皆是基于 AQS 的。
一、自定义锁模拟?
@Slf4j(topic = "e")
public class TestLock {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(()->{
lock.lock();
log.debug("-------------1");
try {
TimeUnit.SECONDS.sleep(3);//让cpu放弃这个线程的调度
log.debug("-------------3");
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
},"t1");
Thread t2 = new Thread(()->{
lock.lock();
log.debug("-------------2");
lock.unlock();
},"t2");
t1.start();
t2.start();
}
}
如上代码,如果我们不使用ReentrantLock锁,由于t1线程中有睡眠,会导致cpu放弃这个调度,继而结果为
22:49:15.437 [t1] DEBUG e - -------------1
22:49:18.447 [t1] DEBUG e - -------------2
22:49:18.447 [t2] DEBUG e - -------------3
如果我们使用锁,则t2需要等待锁释放才能执行,最终结果为
22:49:15.437 [t1] DEBUG e - -------------1
22:49:18.447 [t1] DEBUG e - -------------3
22:49:18.447 [t2] DEBUG e - -------------2
而我们可以先模拟ReentrantLock,先进行简单认识,再深入研究
什么是锁
目标: 同步 多线程间一前一后的执行
它的本质就是一个标识: 如果这个标识改变成了某个状态我们就理解为获取锁
拿不到锁其实就是陷入阻塞(死循环) 让这个方法不返回
自旋实现一个同步
替换上述的ReentrantLock,自定义CustomLock实现clock与cunlock
public class CustomLock {
volatile int status=0;//标识---是否有线程在同步块-----是否有线程上锁成功
private static Unsafe unsafe = null;
private static long statusOffset;
//获取unsafe对象
static {
Field singLeoneInstanceField = null;
try {
singLeoneInstanceField = Unsafe.class.getDeclaredField("theUnsafe");
singLeoneInstanceField.setAccessible(true);
unsafe = (Unsafe)singLeoneInstanceField.get(null);
statusOffset = unsafe.objectFieldOffset(com.juc.CustomLock.class.getDeclaredField("status"));
} catch (Exception e) {
e.printStackTrace();
}
}
public void clock() {
//cas 原子操作
while (!compareAndSet(0, 1)) {
}
}
boolean compareAndSet(int oldVal, int newVal) {
return unsafe.compareAndSwapInt(this, statusOffset, oldVal, newVal);
}
public void cunlock() {
status = 0;
}
}
缺点:耗费cpu资源。没有竞争到锁的线程会一直占用cpu资源进行cas操作,假如一个线程获得锁后要花费Ns处理业务逻辑,那另外一个线程就会白白的花费Ns的cpu资源
解决思路:让得不到锁的线程让出CPU
park+自旋方式实现同步
volatile int status=0;
Queue parkQueue;
void lock(){
while(!compareAndSet(0,1)){
park();
}
unlock()
}
void unlock(){
lock_notify();
}
void park(){
//将当期线程加入到等待队列
parkQueue.add(currentThread);
//将当前线程释放cpu 阻塞
releaseCpu(); //LockSupport.park();
}
void lock_notify(){
//得到要唤醒的线程头部线程
Thread t=parkQueue.header();
//唤醒等待线程
unpark(t); // LockSupport.unpark(t);
}
JDK的JUC包下面ReentrantLock类的原理就是利用了这种机制
ReentrantLock分析
先看加锁流程,再去分析
lock方法的过程(分为公平和非公平锁)
1、获取当前线程
final Thread current = Thread.currentThread();
2、获取锁的状态getState()
int c = getState()
3、判断锁的状态
if(c == 0)
4、如果锁是自由状态则第5步,否则第7步
5、判断自己是否需要排队
什么情况下当前线程不需要排队(排队=入队+阻塞)
1、队列没有初始 对头和队尾等于null的时候不需要排队
2、队列当中只有一个人的时候是不需要排队
6、如果不需要排队–则cas加锁 成功则直接返回;加锁流程结束,执行临界区代码
7、判断是否重入(一般情况下不重入),如果第6步执行,则没有这一步,这是第4步的分支
8、直接返回false(加锁失败,不考虑重入)
9、加锁失败之后会调用addWaiter,主要是入队(入队不等于排队)
入队完成之后第一个节点是虚拟出来的thread等于null的节点,而不是我们入队的节点
为什么这里需要虚拟出来一个点,而不是拿当前节点作为头部?
10、判断是否需要自旋
相关组件定义
FairSync(AQS公平锁)
private transient volatile Node head; //队首
private transient volatile Node tail;//尾
private volatile int state;//锁状态,加锁成功则为1,重入+1 解锁则为0
private transient Thread exclusiveOwnerThread; //当前持有锁线程
Node类的设计
public class Node{
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
acquire方法方法源码分析
public final void acquire(int arg) {
//tryAcquire(arg)尝试加锁,如果加锁失败则会调用acquireQueued方法加入队列去排队
//加入队列之后线程会立马park,等到解锁之后会被unpark,醒来之后判断自己是否被打断了;被打断下次分析
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire方法源码分析
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取lock对象的上锁状态,如果锁是自由状态则=0,如果被上锁则为1,大于1表示重入
int c = getState();
if (c == 0) {
//hasQueuedPredecessors,判断自己是否需要排队
//如果不需要排队则进行cas尝试加锁,如果加锁成功则把当前线程设置为拥有锁的线程
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//设置当前线程为拥有锁的线程
setExclusiveOwnerThread(current);
return true;
}
}
//如果是当前线程,判断是否需要重入
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
hasQueuedPredecessors判断是否需要排队
这个地方会涉及多种情况,放到后面详细说明(不得不佩服大师的高超水平)
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
acquireQueued(addWaiter(Node.exclusive),arg))
代码执行到这里一定是tc需要排队,而出现这种场景有两种情况
1、tf持有了锁,并没有释放,所以tc来加锁的时候需要排队,但这个时候—队列并没有初始化
2、tn持有了锁,那么由于加锁tn!=tf,所以队列是一定被初始化了的,tc来加锁,那么队列当中有人在排队,故而他也去排队
addWaiter(Node mode)
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;
//判断pred是否为空,其实就是判断对尾是否有节点
if (pred != null) {
node.prev = pred;
//这里需要cas,因为防止多个线程加锁,确保nc入队的时候是原子操作
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//入队(会有死循环)
enq(node);
return node;
}
acquireQueued(final Node node, int arg)
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
加锁过程总结
如果是第一个线程tf,那么和队列无关,线程直接持有锁。并且也不会初始化队列,如果接下来的线程都是交替执行,那么永远和AQS队列无关,都是直接线程持有锁,如果发生了竞争,比如tf持有锁的过程中T2来lock,那么这个时候就会初始化AQS,初始化AQS的时候会在队列的头部虚拟一个Thread为NULL的Node,因为队列当中的head永远是持有锁的那个node(除了第一次会虚拟一个,其他时候都是持有锁的那个线程锁封装的node),现在第一次的时候持有锁的是tf而tf不在队列当中所以虚拟了一个node节点,队列当中的除了head之外的所有的node都在park,当tf释放锁之后unpark某个(基本是队列当中的第二个,为什么是第二个呢?前面说过head永远是持有锁的那个node,当有时候也不会是第二个,比如第二个被cancel之后,至于为什么会被cancel,不在我们讨论范围之内,cancel的条件很苛刻,基本不会发生)node之后,node被唤醒,假设node是t2,那么这个时候会首先把t2变成head(sethead),在sethead方法里面会把t2代表的node设置为head,并且把node的Thread设置为null,为什么需要设置null?其实原因很简单,现在t2已经拿到锁了,node就不要排队了,那么node对Thread的引用就没有意义了。所以队列的head里面的Thread永远为null