AQS简介
AQS,全称是AbstractQueuedSynchronizer,是阻塞锁和相关的同步器的框架。
AQS内部核心。
1.state,用过state属性来表示资源的状态,分为独占锁与共享锁,子类会根据这个状态来维护获取锁和释放锁的方式,来决定锁的类型。
getState(),获取当前状态
setState(),给当前状态赋值
compareAndSetState,使用CAS的方式给State来进行赋值。
就像AQS的内部的acquire以及relaese等系列方法,都是针对于AQS的状态来进行更改。
2.Node阻塞队列,内部封装了Node内部类,在对获取不到资源的线程进行阻塞。类似于synchronized中的EntryList。
3.condition,即处于等待状态的线程,类似Synchronized中的waitSet,只不过可以存在多个。
底层的话,核心方法tryAcquire,tryRelease,tryAcquireShared,tryReleaseShared,isHeldExclusively等都是依赖于上述所说的这三点。
自定义不可重入锁
Lock,在java之中,只是一个接口。内部有六个类是需要实现的。lock,lockInterruptibly,newCondition,tryLock,tryLock(带时间参数),unLock方法,这些方法统一都是依赖于AQS来进行实现的。
package com.bo.threadstudy.eight;
import lombok.extern.slf4j.Slf4j;
import java.sql.Time;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
/**
* 测试一下AQS,什么是AQS,AbstractQueuedSynchronizer,内部包含了state状态值,资源的获取以及释放方式,以及一个队列,对获取不到资源的线程阻塞
* 这里是不可重入锁,是compareAnSet直接置为了0,1,其它的锁是在1的基础上无法进入的。
*/
@Slf4j
public class AQSTest {
//定义一个锁,要实现Lock接口,Lock接口中共有6个方法,tryLock,Lock,lockInterruptibly,unlock,newCondition,tryLock加时间参数
public static void main(String[] args) {
MyLock myLock = new MyLock();
new Thread(() -> {
myLock.lock();
try{
log.debug("测试1");
Thread.sleep(1000);
log.debug("测试1结束");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
myLock.unlock();
}
}).start();
new Thread(() -> {
myLock.lock();
try{
log.debug("测试2");
log.debug("测试2结束");
} finally {
myLock.unlock();
}
}).start();
}
}
/**
* Lock中的方法都依赖于AQS来实现的
*/
@Slf4j
class MyLock implements Lock {
private MyAbstractQueuedSynchronizer myAbstractQueuedSynchronizer = new MyAbstractQueuedSynchronizer();
@Override
public void lock() {
//先试图获取锁,获取失败后从队列中来进行获取,如果还没获取到,将当前线程中断,但中断后需要业务逻辑判断中断,然后阻塞,这个判断在哪
myAbstractQueuedSynchronizer.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
myAbstractQueuedSynchronizer.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return myAbstractQueuedSynchronizer.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return myAbstractQueuedSynchronizer.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
myAbstractQueuedSynchronizer.release(1);
}
@Override
public Condition newCondition() {
return myAbstractQueuedSynchronizer.newCondition();
}
}
/**
* 这个抽象方法,把能实现的功能都实现了,虽然只是带了一个抛出异常吧
* 不可重入锁
*/
@Slf4j
class MyAbstractQueuedSynchronizer extends AbstractQueuedSynchronizer{
//默认情况下都是抛出的异常,所以这里需要重写
@Override
protected boolean tryAcquire(int arg) {
//代表当前是要加锁操作
if(arg == 1){
int state = getState();
//如果在中间被其它线程加上锁之后,加锁失败
//TODO 果然,这里应该写0,1,因为在多线程访问呢,如果我用state,state+1,很可能第一个线程读到0,第二个读到1,然后累加全部返回true
//TODO 我上面说的场景有点像共享锁啊,虽然现在要做的是排他锁
if(compareAndSetState(0,1)){
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
return false;
}
/**
* 释放锁的步骤
* @param arg
* @return
*/
@Override
protected boolean tryRelease(int arg) {
if(arg == 1){
if(getState() == 0){
throw new IllegalMonitorStateException("state已经为0");
}
//释放锁资源的过程肯定是单线程操作,所以不需要CAS排进行判定
//当然,如果是重入锁,释放组员肯定不能判断是否等于0来处理了
setState(0);
setExclusiveOwnerThread(null);
return true;
}
return false;
}
/**
* 这个实现的主要目的是为了判断当前线程是否加锁了
* @return
*/
@Override
protected boolean isHeldExclusively() {
//英文意思是是否独占,如果state=1就是独占锁,如果不为1,则可能没被调用,也可能是共享锁
return getState() == 1;
}
protected Condition newCondition() {
return new ConditionObject();
}
}
我自己写了一个MyAbstractQueuedSynchronizer,继承了AQS的底层实现,并依赖它的方法完成了锁的封装。
AQS简单源码
上面的代码,在MyLock类中,实现的几个方法,都是用的AQS自带的一些方法,就像acquire方法,release方法等。我先简单看了一下acquire方法,剩下的在重入锁,读写锁里接着看一下。
在内部首先会试图获取锁,tryAcquire方式,在没有获取到的时候,会将其加入至Node阻塞队列中,调用的是addWaiter方法。
首先,就是把当前线程封装成一个Node节点。
将Node阻塞队列中的尾节点tail,给获取到,并且再创建一个局部变量pred。
判断tail是否为空,如果不为空的话,也就是说,当前内部存在其它正在阻塞的线程。那么,将pred的尾引用指向新存放进来的Node节点。然后采用CAS的方式将tail变量的引用指向了新存放的Node节点(代表了当前传入的Node节点作为最后节点),最终将新传入Node节点的前置节点置为pred对象。
当tail为空时,即阻塞队列内部不存在线程时,进入enq方法。
内部是一个死循环结构,初始状态下,获取到尾节点(因为有可能在这个过程中,其它线程插入了新节点)。然后再次进行CAS判断,查看head预期值是否为空,如果为空的话,那么就先创建一个空节点,并将当前节点也赋值给尾节点。再次循环,执行else里面的代码,将传入节点的prev引用赋值为tail的指针,将需要插入的节点经过CAS判定后,将tail的引用指向新传入的节点,并且原先的tail(现在的倒数第二个节点)的next指向为新插入Node对象。
最终返回这个新插入的Node对象。
acquireQueued方法,默认一个初始的failed方法状态,进入循环后,判断当前节点的头节点是否是队列中第二个元素,如果是的话,因为第一个元素是空的,所以下一个需要被唤醒的线程就是node节点,此时将head引用指向当前节点,并且当前节点之前的node从链表中移除。并返回打断状态为false。如果不是第二个元素的话,则返回一个true的打断状态。最终将当前线程打断。
现在的这种方式,是在程序中调用了Lock方法后会走这个方法,然后应该是在释放锁资源时也会走这个方法。
AQS 的基本思想其实很简单
获取锁的逻辑
while(state 状态不允许获取) {
if(队列中还没有此线程) {
入队并阻塞
}
}
当前线程出队
释放锁的逻辑
if(state 状态允许了) {
恢复阻塞的线程(s)
}
要点
原子维护 state 状态
阻塞及恢复线程
维护队列
1) state 设计
state 使用 volatile 配合 cas 保证其修改时的原子性
state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想
2) 阻塞恢复设计
早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume
那么 suspend 将感知不到
解决方法是使用 park & unpark 来实现线程的暂停和恢复,具体原理在之前讲过了,先 unpark 再 park 也没
问题
park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
park 线程还可以通过 interrupt 打断
3) 队列设计
使用了 FIFO 先入先出队列,并不支持优先级队列
设计时借鉴了 CLH 队列,它是一种单向无锁队列
还是有点模糊,接下来还得好好的看一下,头疼啊。面试假设问到这程度,没法玩了。