AQS原理
全称是AbstractQueuedSynchronizer,是阻塞式锁和相关同步器的框架
特点:
- 使用state属性去表示资源的状态(分为独占和共享),子类需要去定义如何去维护这个状态,控制如何获取锁和释放锁
- getState:获取state状态
- setState:设置state状态
- compareAndSetState:用CAS机制设置state状态
- 独占模式是只允许一个线程去访问共享变量,而共享模式可以运行多个线程去访问共享变量
- 提供了基于FIFO等待队列(java实现),类似于Monitor的EntryList(c++实现)
- 条件变量来实现等待,唤醒机制,支持多个条件变量,类似于Monitor的WaitSet
子类主要实现的方法(默认抛出UnspoortOperationException):
- tryAcquire:尝试获取锁——只会尝试一次
- tryRelease: 尝试释放释放锁
注意:AQS中去阻塞和恢复阻塞使用的是park/unpark机制
下面我们去使用AQS自定义一个不可重入锁
class MyLock implements Lock{
//独占锁 同步器类
class MySync extends AbstractQueuedSynchronizer{
@Override//尝试获取锁
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0,1)){
//加上了锁,并且设置owner为当前线程
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override//尝试释放锁
protected boolean tryRelease(int arg) {
//因为释放锁不会发生锁竞争,所以不用使用CAS
setExclusiveOwnerThread(null);
//注意:State加了voletile关键字,所以把它放在后面修改,添加写屏障,确保前面的不可重排
setState(0);
return true;
}
@Override
protected boolean isHeldExclusively() {
return getState() ==1;
}
private Condition newCondition(){
return new ConditionObject();
}
}
private MySync sync = new MySync();
@Override //加锁
public void lock() {
sync.acquire(1);//会调用tryAcquire方法(不成功则进入等待队列)
}
@Override //加锁,可打断
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override //尝试获取锁
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override //尝试获取锁,带超时时间
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(2));
}
@Override //解锁
public void unlock() {
//release会不仅会调用tryRelaese将状态置为0,Owner置为null,
//还会唤醒阻塞队列中阻塞的线程
sync.release(1);
}
@Override //新建条件变量
public Condition newCondition() {
return sync.newCondition();
}
}
ReentrantLock
ReentrantLock内部也维护了一个继承于AQS的sync类,但是它是抽象的,子类有公平Sync和非公平Sync
非公平锁的实现原理
加锁解锁流程
从构造器看,ReentrantLock默认为非公平锁
加锁
调用的是sync的lock方法,如下是非公平锁的lock方法源码:
当没有竞争时如下图所示:
当第一个竞争出现的时候:
- 当CAS尝试将State由0修改为1,结果失败了
- 然后回进入tryAcquire逻辑,这时state已经是1,结果仍然失败
- 然后会执行与语句中的addWaiter逻辑,再head中构造Node队列
-
- 下图中的黄色三角表示该node的waitStatus状态,其中0为默认正常状态
- Node是懒惰创建的
- 第一次创建的时候会创建两个Node节点,第一个是空的,称为哨兵,用来占位,并不关联线程
- 此时当前线程还是活动的
acquire代码如下:
当前线程进入acquireQueued逻辑
- acuireQueued会在一个死循环中不断尝试获得锁,失败后进入park阻塞
- 如果当前线程的前一个结点是head,那么再次tryAcquire尝试获取锁,当然这时state仍然为1,失败
- 进入shouldParkAfterFailedAcquire逻辑,将前驱node,即head的waitStatus改为-1(表示当前线程进入阻塞,且该结点有责任将当前线程唤醒),返回false
- shouldParkAfterFailedAcquire执行完之后,又循环再次tryAcquired尝试获取锁,这时任然失败
- 当再次进入shouldParkAfterFailedAcquire的时候,这时前驱结点的waitStatus已经为-1,这次返回true
- 进入parkAndCheckInterrupt,Thread-1 park被阻塞,Thread-1 park用灰色标记
acquireQueued源代码如下所示:
再次有多个线程经历上述过程竞争失败,就会变成下图这样
然后当Thread-0释放锁时,进入tryRelease流程,如果成功
- 设置exclusiveOwnerThread为null
- 设置status为0
如果当前队列不为null,并且head的waitStatus=-1,进入unparkSuccessor流程,找到队列中离head最近的一个Node,unpark恢复其运行Thread-1,会把当前结点设置为哨兵结点,且相关信息设置为null,前面的哨兵结点移除
回到Thread-1的acquiredQueued流程
如果加锁成功(没有竞争),会设置
- exclusiveOwnerThread为Thread-1,state=1
- head指向刚刚的Thread-1所在的node,该node被清空Thread
- 原本的head因为从链表断开,而可以被垃圾回收
但是如果这时候有其他线程过来竞争(非公平锁的体现),例如这时Thread-4来了
如果不巧又被Thread-4占了先
- Thread-4被设置为exclusiveOwnerThread,state=1
- Thread-1再次进入acquireQueued流程,获取锁失败,重新进入park阻塞
锁重入的原理
尝试加锁的时候会获取到State,如果已经获取了锁,线程还是当前线程,就表示发生了锁重入,会将state自增然后返回true
尝试释放锁时,先将state-1,设置一个标记位位false,如果c==0的话再将标记设置为true,然后将OwnerThread设置为null,否则的话返回false,表示只是让锁重入的计数-1,而不是真正释放锁
可打断原理
在默认情况下,ReentrantLock是不可打断模式,即使被打断,再在将打断标记设置为真之后,仍然会循环尝试获取锁,只有获取锁成功之后才会返回打断标记,进行打断
我们来看看可打断模式的源码:
可打断锁在阻塞被打断之后,会抛出异常,而不是设置打断标记,抛出异常就会终止循环,停止等等锁
公平锁的原理
非公平锁是因为插队,锁刚放开,队列中的线程还没有被唤醒,或者唤醒了还没有去抢占锁,突然新来的线程插队拿到了锁
公平锁和非公平锁的区别:
条件变量实现原理
每个条件变量就对应着一个等待队列,其实现类是ConditionObject
await流程
开始Thread-0持有锁,如果调用await进入ConditionObject的addConditionWaiter流程创建新的Node状态为-2(Node.CONDITION),关联Thread-0,加入等待队列尾部
await的源码如下:
addConditionWaiter的源码如下:
然后会进入fullyRelease流程将NonfairSync中的state设置为0,OwnerThread置为null,释放同步器上的锁
fullyRelease里会调用release方法,unparkAQS队列中的下一个节点,竞争锁,假设没有其他的竞争线程,那么Thread-1竞争成功
release源码如下:
然后会调用park方法将当前线程Thread-0阻塞住
signal
假设线程1要唤醒线程0,那么就需要调用signal方法,signal方法首先会判断是否是锁的持有者,且判断阻塞队列中的第一个Node是否为空,如果不为空会去调用dosignal方法
signal源码如下:
进入ConditionObjecy的doSignal流程,取得条件变量中的第一个Node,就是Thread-0所在的Node,将它从双向链表中断开
然后执行transferForSignal流程,将Node加入AQS的队列尾部,将Thread-0的WaitStatus改为0,Thread-3的waitStatus改为-1
doSignal的源码如下:
图片素材from黑马程序员