在JDK1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6后,进行大量的锁优化策略,但是与Lock相比synchronized还是存在一些缺陷的:虽然synchronized提供了便捷性的隐式获取锁释放锁机制(基于JVM机制),但是它却缺少了获取锁与释放锁的可操作性,可中断、超时获取锁,且它为独占式在高并发场景下性能大打折扣。
park+自旋方式实现同步
volatile int status=0;
Queue parkQueue;//集合 数组 list
void lock(){
while(!compareAndSet(0,1)){
//
park();
}
//lock 10分钟
。。。。。。
unlock();
}
void unlock(){
lock_notify();
}
void park(){
//将当期线程加入到等待队列
parkQueue.add(currentThread);
//将当期线程释放cpu 阻塞
releaseCpu();
}
void lock_notify(){
//得到要唤醒的线程头部线程
Thread t=parkQueue.header();
//唤醒等待线程
unpark(t);
}
ReentrantLock结构组成
首先ReentrantLock
实现了Lock
接口,Lock
接口是Java
中对锁操作行为的统一规范,Lock
接口的定义如下
public interface Lock {
/**
* 获取锁
*/
void lock();
/**
* 获取锁-响应中断
*/
void lockInterruptibly() throws InterruptedException;
/**
* 返回获取锁是否成功状态
*/
boolean tryLock();
/**
* 返回获取锁是否成功状态-响应中断
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 释放锁
*/
void unlock();
/**
* 创建条件变量
*/
Condition newCondition();
}
Lock
接口定义的函数不多,接下来ReentrantLock
要去实现这些函数,遵循着解耦可扩展设计,ReentrantLock
内部定义了专门的组件Sync
, Sync
继承AbstractQueuedSynchronizer
提供释放资源的实现,NonfairSync
和FairSync
是基于Sync
扩展的子类,即ReentrantLock
的非公平模式与公平模式,它们作为Lock
接口功能的基本实现。
大白话来说,企业的老板,为了响应政府的政策,需要对企业内部做调整,但是政府每年政策都不一样,每次都要自己去亲力亲为,索性长痛不如短痛,专门成立一个政策应对部门,以后这些事情都交予这个部门去做,老板只需要指挥它们就好了。
Sync
Sync
可以说是ReentrantLock
的亲儿子,它寄托了全村的希望,完美的继承了AbstractQueuedSynchronizer
,是ReentrantLock
的核心,后面的NonfairSync
与FairSync
都是基于Sync
扩展出来的子类。
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* 获取锁-子类实现
*/
abstract void lock();
/**
* 非公平-获取资源
*/
final boolean nonfairTryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取当前状态
int c = getState();
if (c == 0) { // state==0 代表资源可获取
//cas设置state为acquires,acquires传入的是1
if (compareAndSetState(0, acquires)) {
//cas成功,设置当前持有锁的线程
setExclusiveOwnerThread(current);
//返回成功
return true;
}
}
else if (current == getExclusiveOwnerThread()) { //如果state!=0,但是当前线程是持有锁线程,直接重入
//state状态+1
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//设置state状态,此处不需要cas,因为持有锁的线程只有一个
setState(nextc);
//返回成功
return true;
}
//返回失败
return false;
}
/**
* 释放资源
*/
protected final boolean tryRelease(int releases) {
//state状态-releases,releases传入的是1
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread()) //如果当前线程不是持有锁线程,抛出异常
throw new IllegalMonitorStateException();
//设置返回状态,默认为失败
boolean free = false;
if (c == 0) {//state-1后,如果c==0代表释放资源成功
//返回状态设置为true
free = true;
//清空持有锁线程
setExclusiveOwnerThread(null);
}
//如果state-1后,state还是>0,代表当前线程有锁重入操作,需要做相应的释放次数,设置state值
setState(c);
return free;
}
}
Sync
逻辑都比较简单,实现了A Q S
类的释放资源(tryRelease),然后抽象了一个获取锁的函数让子类自行实现(lock),再加一个偏心的函数nonfairTryAcquire
,
NonfairSync
简单的说下A Q S(AbstractQueuedSynchronizer)
流程,A Q S
为加锁和解锁过程提供了统一的模板函数,加锁与解锁的模板流程是,获取锁失败的线程,会进入CLH
队列阻塞,其他线程解锁会唤醒CLH
队列线程
上图中,线程释放锁时,会唤醒CLH
队列阻塞的线程,重新竞争锁,要注意,此时可能还有非CLH
队列的线程参与竞争,所以非公平就体现在这里,非CLH
队列线程与CLH
队列线程竞争,各凭本事,不会因为你是CLH
队列的线程,排了很久的队,就把锁让给你。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* 获取锁
*/
final void lock() {
if (compareAndSetState(0, 1))//cas设置state为1成功,代表获取资源成功
//资源获取成功,设置当前线程为持有锁线程
setExclusiveOwnerThread(Thread.currentThread());
else
//cas设置state为1失败,代表获取资源失败,执行AQS获取锁模板流程,否获取资源成功
acquire(1);
}
/**
* 获取资源-使用的是Sync提供的nonfairTryAcquire函数
*/
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* AQS获取锁模板函数,这是AQS类中的函数
*/
public final void acquire(int arg) {
/**
* 我们只需要关注tryAcquire函数,后面的函数是AQS获取资源失败,线程节点进入CLH队列的细节流程,本文不关注
*/
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先A Q S
的acquire
函数是获取锁的流程模板,模板流程会先执行tryAcquire
函数获取资源,tryAcquire
函数要子类实现,NonfairSync
作为子类,实现了tryAcquire
函数,具体实现是调用了Sync
的nonfairTryAcquire
函数。
FairSync
所谓公平策略就是,严格按照CLH
队列顺序获取锁,线程释放锁时,会唤醒CLH
队列阻塞的线程,重新竞争锁,要注意,此时可能还有非CLH
队列的线程参与竞争,为了保证公平,一定会让CLH
队列线程竞争成功,如果非CLH
队列线程一直占用时间片,那就一直失败(构建成节点插入到CLH
队尾,由A S Q
模板流程执行),直到时间片轮到CLH
队列线程为止,所以公平策略的性能会更差。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
/**
* 获取锁
*/
final void lock() {
//cas设置state为1失败,代表获取资源失败,执行AQS获取锁模板流程,否获取资源成功
acquire(1);
}
/**
* 获取资源
*/
protected final boolean tryAcquire(int acquires) {
//获取当前线程
final Thread current = Thread.currentThread();
//获取state状态
int c = getState();
if (c == 0) { // state==0 代表资源可获取
//1.hasQueuedPredecessors判断当前线程是不是CLH队列被唤醒的线程,如果是执行下一个步骤
//2.cas设置state为acquires,acquires传入的是1
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//cas成功,设置当前持有锁的线程
setExclusiveOwnerThread(current);
//返回成功
return true;
}
}
//如果state!=0,但是当前线程是持有锁线程,直接重入
else if (current == getExclusiveOwnerThread()) {
//state状态+1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
//设置state状态,此处不需要cas,因为持有锁的线程只有一个
setState(nextc);
//返回成功
return true;
}
return false;
}
}
/**
* AQS获取锁模板函数,这是AQS类中的函数
*/
public final void acquire(int arg) {
/**
* 我们只需要关注tryAcquire函数,后面的函数是AQS获取资源失败,线程节点进入CLH队列的细节流程,本文不关注
*/
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
其实我们不难发现FairSync
流程与NonfairSync
基本一致,唯一的区别就是在C A S
执行前,多了一步hasQueuedPredecessors
函数,这一步就是判断当前线程是不是CLH
队列被唤醒的线程,如果是就执行C A S
,否则获取资源失败。
Lock的实现
//同步器
private final Sync sync;
//默认使用非公平策略
public ReentrantLock() {
sync = new NonfairSync();
}
//true-公平策略 false非公平策略
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock
默认是使用非公平策略,如果想指定模式,可以通过入参fair
来选择,这里就不做过多概述,接下来看看ReentrantLock
对Lock
的实现
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
//同步器
private final Sync sync;
//默认使用非公平策略
public ReentrantLock() {
sync = new NonfairSync();
}
//true-公平策略 false非公平策略
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
/**
* 获取锁-阻塞
*/
public void lock() {
//基于sync实现
sync.lock();
}
/**
* 获取锁-阻塞,支持响应线程中断
*/
public void lockInterruptibly() throws InterruptedException {
//基于sync实现
sync.acquireInterruptibly(1);
}
/**
* 获取资源,返回是否成功状态-非阻塞
*/
public boolean tryLock() {
//基于sync实现
return sync.nonfairTryAcquire(1);
}
/**
* 获取锁-阻塞,支持超时
*/
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
//基于sync实现
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
/**
* 释放锁
*/
public void unlock() {
//基于sync实现
sync.release(1);
}
/**
* 创建条件变量
*/
public Condition newCondition() {
//基于sync实现
return sync.newCondition();
}
}
ReentrantLock
对Lock
的实现都是基于Sync
来做的,有一种神器在手,天下我有的风范。
Sync
承包了所有事情,为何它如此牛皮,因为Sync
上有AbstractQueuedSynchronizer
老大哥罩着,下有NonfairSync
与FairSync
两小弟可差遣,所以成为ReentrantLock
的利器也合情合理。
AQS
private transient volatile Node head; //队首
private transient volatile Node tail;//尾
private volatile int state;//锁状态,加锁成功则为1,重入+1 解锁则为0
public class Node{
volatile Node prev;
volatile Node next;
volatile Thread thread;
}
int CANCELLED = 1;//waitStatus值为1时表示该线程节点已释放(超时、中断),已取消的节点不会再阻塞。
int SIGNAL = -1;//waitStatus为-1时表示该线程的后续线程需要阻塞,即只要前置节点释放锁,就会通知标识为 SIGNAL 状态的后续节点的线程
int CONDITION = -2; //waitStatus为-2时,表示该线程在condition队列中阻塞(Condition有使用)
int PROPAGATE = -3;//waitStatus为-3时,表示该线程以及后续线程进行无条件传播(CountDownLatch中有使用)共享模式下, PROPAGATE 状态的线程处于可运行状态
waitStatus:仅仅是一个状态而已;ws是一个过渡状态,在不同方法里面判断ws的状态做不同的处理,所以ws=0有其存在的必要性
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) {//没人占用锁--->我要去上锁----1、锁是自由状态
//hasQueuedPredecessors,判断自己是否需要排队这个方法比较复杂,
//下面我会单独介绍,如果不需要排队则进行cas尝试加锁,如果加锁成功则把当前线程设置为拥有锁的线程
//继而返回true
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//设置当前线程为拥有锁的线程,方面后面判断是不是重入(只需把这个线程拿出来判断是否当前线程即可判断重入)
setExclusiveOwnerThread(current);
return true;
}
}
//如果C不等于0,而且当前线程不等于拥有锁的线程则不会进else if 直接返回false,加锁失败
//如果C不等于0,但是当前线程等于拥有锁的线程则表示这是一次重入,那么直接把状态+1表示重入次数+1
//那么这里也侧面说明了reentrantlock是可以重入的,因为如果是重入也返回true,也能lock成功
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判断是否需要排队的源码分析
tail:队列的队尾 head:队列的对首
ts:第二个给lock加锁的线程
tf:第一个给lock加锁的线程
tc:当前给线程加锁的线程
tl:最后一个加锁的线程
tn:随便某个线程
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
/**
* 下面提到的所有不需要排队
* 一:队列没有初始化,不需要排队;直接去加锁,但是可能会失败;
* 假设两个线程同时来lock,都看到队列没有初始化,都认为不需要排队,都去进行CAS修改计数器;有一个必然失败
* 比如t1先拿到锁,那么另外一个t2则会CAS失败,这个时候t2就会去初始化队列,并排队
*
* 二:队列被初始化了,但是tc过来加锁,发觉队列当中第一个排队的就是自己;比如重入;
* 那么什么叫做第一个排队的呢?下面解释了,很重要往下看;
* 这个时候他也不需要排队;为什么不需要排对?
* 因为队列当中第一个排队的线程他会去尝试获取一下锁,因为有可能这个时候持有锁的那个线程可能释放了锁;
* 如果释放了就直接获取锁执行。但是如果没有释放他就会去排队,
* 所以这里的不需要排队,不是真的不需要排队
*
*
* h != t 判断首不等于尾这里要分三种情况
* 1、队列没有初始化,也就是第一个线程tf来加锁的时候那么这个时候队列没有初始化,
* h和t都是null,那么这个时候判断不等于则不成立(false)那么由于是&&运算后面的就不会走了,
* 直接返回false表示不需要排队,而前面又是取反(if (!hasQueuedPredecessors()),所以会直接去cas加锁。
* ----------第一种情况总结:队列没有初始化没人排队,那么我直接不排队,直接上锁;合情合理、有理有据令人信服;
* 好比你去火车站买票,服务员都闲的蛋疼,整个队列都没有形成;没人排队,你直接过去交钱拿票
*
* 2、队列被初始化了,后面会分析队列初始化的流程,如果队列被初始化那么h!=t则成立;(不绝对,还有第3中情况)
* h != t 返回true;但是由于是&&运算,故而代码还需要进行后续的判断
* (有人可能会疑问,比如队列初始化了;里面只有一个数据,那么头和尾都是同一个怎么会成立呢?
* 其实这是第3种情况--对头等于对尾;但是这里先不考虑,我们假设现在队列里面有大于1个数据)
* 大于1个数据则成立;继续判断把h.next赋值给s;s有是对头的下一个Node,
* 这个时候s则表示他是队列当中参与排队的线程而且是排在最前面的;
* 为什么是s最前面不是h嘛?诚然h是队列里面的第一个,但是不是排队的第一个;下文有详细解释
* 因为h也就是对头对应的Node对象或者线程他是持有锁的,但是不参与排队;
* 这个很好理解,比如你去买车票,你如果是第一个这个时候售票员已经在给你服务了,你不算排队,你后面的才算排队;
* 队列里面的h是不参与排队的这点一定要明白;参考下面关于队列初始化的解释;
* 因为h要么是虚拟出来的节点,要么是持有锁的节点;什么时候是虚拟的呢?什么时候是持有锁的节点呢?下文分析
* 然后判断s是否等于空,其实就是判断队列里面是否只有一个数据;
* 假设队列大于1个,那么肯定不成立(s==null---->false),因为大于一个Node的时候h.next肯定不为空;
* 由于是||运算如果返回false,还要判断s.thread != Thread.currentThread();这里又分为两种情况
* 2.1 s.thread != Thread.currentThread() 返回true,就是当前线程不等于在排队的第一个线程s;
* 那么这个时候整体结果就是h!=t:true; (s==null false || s.thread != Thread.currentThread() true 最后true)
* 结果: true && true 方法最终放回true,所以需要去排队
* 其实这样符合情理,试想一下买火车票,队列不为空,有人在排队;
* 而且第一个排队的人和现在来参与竞争的人不是同一个,那么你就乖乖去排队
* 2.2 s.thread != Thread.currentThread() 返回false 表示当前来参与竞争锁的线程和第一个排队的线程是同一个线程
* 这个时候整体结果就是h!=t---->true; (s==null false || s.thread != Thread.currentThread() false-----> 最后false)
* 结果:true && false 方法最终放回false,所以不需要去排队
* 不需要排队则调用 compareAndSetState(0, acquires) 去改变计数器尝试上锁;
* 这里又分为两种情况(日了狗了这一行代码;有同学课后反应说子路老师老师老是说这个AQS难,
* 你现在仔细看看这一行代码的意义,真的不简单的)
* 2.2.1 第一种情况加锁成功?有人会问为什么会成功啊,如这个时候h也就是持有锁的那个线程执行完了
* 释放锁了,那么肯定成功啊;成功则执行 setExclusiveOwnerThread(current); 然后返回true 自己看代码
* 2.2.2 第二种情况加锁失败?有人会问为什么会失败啊。假如这个时候h也就是持有锁的那个线程没执行完
* 没释放锁,那么肯定失败啊;失败则直接返回false,不会进else if(else if是相对于 if (c == 0)的)
* 那么如果失败怎么办呢?后面分析;
*
*----------第二种情况总结,如果队列被初始化了,而且至少有一个人在排队那么自己也去排队;但是有个插曲;
* ----------他会去看看那个第一个排队的人是不是自己,如果是自己那么他就去尝试加锁;尝试看看锁有没有释放
*----------也合情合理,好比你去买票,如果有人排队,那么你乖乖排队,但是你会去看第一个排队的人是不是你女朋友;
*----------如果是你女朋友就相当于是你自己(这里实在想不出现实世界关于重入的例子,只能用男女朋友来替代);
* --------- 你就叫你女朋友看看售票员有没有搞完,有没有轮到你女朋友,因为你女朋友是第一个排队的
* 疑问:比如如果在在排队,那么他是park状态,如果是park状态,自己怎么还可能重入啊。
* 希望有同学可以想出来为什么和我讨论一下,作为一个菜逼,希望有人教教我
*
*
* 3、队列被初始化了,但是里面只有一个数据;什么情况下才会出现这种情况呢?ts加锁的时候里面就只有一个数据?
* 其实不是,因为队列初始化的时候会虚拟一个h作为头结点,tc=ts作为第一个排队的节点;tf为持有锁的节点
* 为什么这么做呢?因为AQS认为h永远是不排队的,假设你不虚拟节点出来那么ts就是h,
* 而ts其实需要排队的,因为这个时候tf可能没有执行完,还持有着锁,ts得不到锁,故而他需要排队;
* 那么为什么要虚拟为什么ts不直接排在tf之后呢,上面已经时说明白了,tf来上锁的时候队列都没有,他不进队列,
* 故而ts无法排在tf之后,只能虚拟一个thread=null的节点出来(Node对象当中的thread为null);
* 那么问题来了;究竟什么时候会出现队列当中只有一个数据呢?假设原队列里面有5个人在排队,当前面4个都执行完了
* 轮到第五个线程得到锁的时候;他会把自己设置成为头部,而尾部又没有,故而队列当中只有一个h就是第五个
* 至于为什么需要把自己设置成头部;其实已经解释了,因为这个时候五个线程已经不排队了,他拿到锁了;
* 所以他不参与排队,故而需要设置成为h;即头部;所以这个时间内,队列当中只有一个节点
* 关于加锁成功后把自己设置成为头部的源码,后面会解析到;继续第三种情况的代码分析
* 记得这个时候队列已经初始化了,但是只有一个数据,并且这个数据所代表的线程是持有锁
* h != t false 由于后面是&&运算,故而返回false可以不参与运算,整个方法返回false;不需要排队
*
*
*-------------第三种情况总结:如果队列当中只有一个节点,而这种情况我们分析了,
*-------------这个节点就是当前持有锁的那个节点,故而我不需要排队,进行cas;尝试加锁
*-------------这是AQS的设计原理,他会判断你入队之前,队列里面有没有人排队;
*-------------有没有人排队分两种情况;队列没有初始化,不需要排队
*--------------队列初始化了,按时只有一个节点,也是没人排队,自己先也不排队
*--------------只要认定自己不需要排队,则先尝试加锁;加锁失败之后再排队;
*--------------再一次解释了不需要排队这个词的歧义性
*-------------如果加锁失败了,在去park,下文有详细解释这样设计源码和原因
*-------------如果持有锁的线程释放了锁,那么我能成功上锁
*
**/
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}