ReentrantLock 是 java.util.concurrent(简称 J.U.C)下面的一个工具类,他是基于AbstractQueuedSynchronized(简称AQS)实现的,有公平锁与非公平锁,Reentranlock 默认实现为非公平锁,在高竞争的条件下有更好的性能。
锁是用来解决多线程并发访问共享资源所带来的数据安全性问题的手段。对一个共享资源加锁后,如果有一个线程获得了锁,那么其他线程无法访问这个共享资源。
一个持有锁的线程,在释放锁之前,如果再次访问加了该同步锁的其他方法,这个线程不需要再次争抢,只需要记录重入次数。
如果懒得看源码,可以直接跳到总结看流程图讲解
Lock lock = new ReentrantLock();
lock.lock();
lock.unlock();
/** 定义同步器,实现有公平锁与非公平锁 */
private final Sync sync;
/**
* ReentrantLock 构造方法
* 默认实现为 NonfairSync(非公平锁)
*/
public ReentrantLock() {
sync = new NonfairSync();
}
/**
* 非公平锁为内部类
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
lock.lock(); //调用NonfairSync下的lock方法
// NonfairSync下的lock方法
void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
首先进入 compareAndSetState 方法,翻译一下就是比较并且替换,这个方法非常重要,也就是咱们经常说的CAS锁,也称为乐观锁。CAS 包含三个操作数 内存位置(V)、预期原值(A)和新值(B)。
其实是一种无锁化的加锁设计,原理为如果内存位置(V)的值与预期原值(A)相匹配,那么处理器会自动将该位置值更新为新值(B) 。
听起来有点复杂,拿去找朋友玩耍例子来比喻一下,去朋友家(内存位置V),我带着他在家的预期(预期原值A)去找他玩,到了之后敲门看看他在不在家(与预期值进行比较匹配),如果朋友在家我进门一起玩耍(新值B)。
CAS的应用十分广泛,例如MySql中也有类似的设计,通过版本号进行对比,是否为最新数据等等
void lock() {
compareAndSetState(0, 1)
}
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
跟到这里后看到native就不必在继续了,毕竟实现都是由JVM处理的,从上面的参数可以看出 unsafe.compareAndSwapInt 有四个值,
this : 当前类的内存地址
stateOffset : 内存偏移量
this + stateOffset 为当前类地址的偏移量,即为锁所在的位置(内存位置V)
expect:预期值(预期原值A)
update:修改后的值(新值B)
CAS判断后,如果加锁成功则进入setExclusiveOwnerThread方法,加锁不成功则进入acquire方法,咱们先去看成功的源码
void lock() {
setExclusiveOwnerThread(Thread.currentThread());
}
protected final void setExclusiveOwnerThread(Thread thread) {
//设置当前拥有独占访问权限的线程。
exclusiveOwnerThread = thread;
}
emmm ,没错 !就这!
毕竟都上锁了,还要乱七八糟的方法干嘛,所以这里只是记录了获得锁的一下线程。
第一次进入该方法的线程就算成功获得到锁,那么后来进入的线程,则要去acquire方法里
void lock() {
//注意这里 传入的值是1
acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
小小的判断缺分为了三个大的方法(源码就是为了看不懂而存在的)。
如果都为TURE后进入selfInterrupt();
1、tryAcquire(arg);
2、addWaiter(Node.EXCLUSIVE)
3、acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
4、selfInterrupt();
首先看第一个tryAcquire(arg) ,这是一个抽象父类的方法而父类本身不提供实现交由子类去实现,所以在父类中直接抛异常(模板模式)。
所以找到非公平锁的实现方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
//获得当前为抢占锁的线程
final Thread current = Thread.currentThread();
//获取线程的同步状态,判断锁的重入次数
int c = getState();
//c==0 未获得锁的线程
if (c == 0) {
//CAS操作去进行锁抢占 此时acquires=1
if (compareAndSetState(0, acquires)) {
//如果抢占成功则写入当前线程并且返回TRUE
setExclusiveOwnerThread(current);
return true;
}
}else if (current == getExclusiveOwnerThread()) {
//如果当前线程等于独占的线程,已经获得锁之后,还要获得锁表示进行锁重入 acquires = 1 重入锁特性
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//写入当前重入次数
setState(nextc);
return true;
}
return false;
}
按照刚刚的推论,已经有一个线程A获得了锁,那么其他线程进进入 c==0 判断,然后去进行CAS操作抢占锁,发现没成功 则返回fasle
因为外层判断为取反操作,则进入到acquireQueued(addWaiter(Node.EXCLUSIVE), arg)),其实这里也很好理解,抢占到锁的线程,或者是重入的线程,直接返回就可以了,没必要进行下一步操作,抢占不成功,则进入等待队列
/** 标记指示节点正在独占模式下等待*/
static final Node EXCLUSIVE = null;
addWaiter(Node.EXCLUSIVE) -> addWaiter(null)
private Node addWaiter(Node mode) {
//创建新的节点
Node node = new Node(Thread.currentThread(), mode);
//判断尾节点是否为空,初始化的队列tail为NULL
Node pred = tail;
if (pred != null) {
//如果尾节点不为空,则说明链表初始化完毕,直接替换尾部节点即可,详情可以看enq方法的解析
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//第一次进入等待队列的线程,走该方法,传入新建好的node节点
enq(node);
return node;
}
private Node enq(final Node node) {
//自旋操作
for (;;) {
//获取尾节点,因为在多线程环境中访问顺序是不可控的,所以要多几次判断来确定是否为空
Node t = tail;
//尾节点为空则表示未初始化链表,不为空则为链表初始化完毕
if (t == null) {
//如果尾节点为空,则进行链表的初始化,这里也是unsafe类中的方法,创建一个空节点
if (compareAndSetHead(new Node()))
//设置头尾相等,初始化完毕,跳出if判断进行下一次循环
tail = head;
} else {
//获取当前节点的上一个节点
node.prev = t;
//CAS操作替换尾部节点,预期值为t尾节点,修改值为node节点
if (compareAndSetTail(t, node)) {
//尾部节点进行替换
t.next = node;
//返回当前的节点,跳出循环
return t;
}
}
}
}
//node的构造方法
Node(Thread thread, Node mode) {
// Used by addWaiter
//下一个等待节点为 mode=null
this.nextWaiter = mode;
//当前的线程
this.thread = thread;
}
//CAS操作,比较替换头节点,预期值是NULL,修改后为当前节点,因为直接操作内存,所以可以保证原子性
private final boolean compareAndSetHead(Node update) {
return unsafe.compareAndSwapObject(this, headOffset, null, update);
}
//CAS操作,比较替换尾节点,预期值是当前节点的上节点
private final boolean compareAndSetTail(Node expect, Node update) {
return unsafe.compareAndSwapObject(this, tailOffset, expect, update);
}
简单的说明就是,没成功抢占到锁的线程需要进入到等待队列进行等待,首先创建Node节点记录当前线程,进行链表初始化,即头结点与尾节点都为当前线程节点,表示等待队列链表初始化成功,这里有一个优化,已经初始化的链表不需要进入到enq方法直接插入尾节点即可。
通过上方一系列的操作,返回Node节点,作为值传入acquireQueued()方法中
void acquire(int arg) {
Node node = addWaiter(Node.EXCLUSIVE);
acquireQueued(node, arg)//简化后的写法
}
//注意这是使用了final修饰方法 修饰了Node节点 arg = 1
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//又是一个自旋操作
for (;;) {
//获取P节点 为当前节点的上节点
//node.predecessor 注释1
final Node p = node.predecessor();
//该方法验证了 当前节点的上节点是否为头节点,如果是头节点,则可以进行锁的抢占
//tryAcquire方法在前面也讲过了,非公平锁的特性真是无处不在
if (p == head && tryAcquire(arg)) {
//抢占锁成功后,设置头结点为当前节点
//setHead 注释2
setHead(node);
//然后断开原来的头结点,使得可以GC回收
p.next = null; // help GC
//返回fasle
failed = false;
return interrupted;
}
//如果不是头结点,或者未抢占到锁,则进入该方法
//shouldParkAfterFailedAcquire 注释3
//parkAndCheckInterrupt 注释4
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
//如果为ture 则进入取消,只有在节点异常时才会启用取消状态
if (failed)
cancelAcquire(node);
}
}
//注释1:获得当前节点的上节点,这个··没什么好说的
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
//注释2
private void setHead(Node node) {
//头结点为当前节点
head = node;
//当前节点线程置空
node.thread = null;
//当前节点上节点置空
node.prev = null;
}
//注释3 字面意思也很好理解,尝试 挂起 之后 失败的 队列 pred = node的上节点
/**
* 状态字段
* SIGNAL: 处于可以被唤醒的状态 -1
* CANCELLED: 取消状态 1
* CONDITION: 条件队列专属,值为 -2
* PROPAGATE: 默认值 -3 (不知道干嘛的 怪我学艺不精)
* 0: 默认值 0
*/
volatile int waitStatus;
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获得上节点的状态,默认值为0
int ws = pred.waitStatus;
//如果ws值等于-1 说明当前线程处于可以唤醒的状态
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
//如果大于0,只有一种可能就是被需要
do {
//断掉当前节点,重新连接后面的节点
//当前节点的上节点 = 上节点 = 上节点的上节点(反向套娃)
node.prev = pred = pred.prev;
//如果当前的节点还是1为需要状态,那继续循环知道获取到状态不为1的节点
} while (pred.waitStatus > 0);
//设置上节点的下节点 等于当前节点,因为又取消后的节点,所以需要重新复制
pred.next = node;
} else {
//如果当前的节点状态小于0,说明为默认,为条件队列,为-3(这个我真不知道干嘛的)
//进行CAS操作,替换当前线程为可唤醒状态,修改后 跳出方法,下一次再进入时返回true
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
//返回false 跳出方法继续进行自旋循环
return false;
}
//注释4 挂起并且检查是否有中断标记
private final boolean parkAndCheckInterrupt() {
//挂起当前节点,释放cpu资源
LockSupport.park(this);
//中断标记:默认为false,如果为TRUE 则进入 finally中的cancelAcquire方法
return Thread.interrupted();
}
这一段是真的长,简单总结一下,在addWaiter 构建好链表之后,进入acquireQueued首先进行CAS抢占锁,失败后开始挂起未获得锁的线程,并且修改一下节点的状态为可唤醒的,最后挂起线程,释放CPU的资源。
public void unlock() {
//从这里可以大概猜出,同步器记录释放锁,传入参数1,如果当前没有重入则直接释放,如果有重入,则重入次数-1
sync.release(1);
}
//释放锁资源 arg = 1
public final boolean release(int arg) {
//尝试释放锁,改方法也是模板方法,父类不提供实现 交由子类去实现,所以找到非公平锁的实现
//注释1
if (tryRelease(arg)) {
//获取头结点
Node h = head;
//如果头结点不为空,说明等待队列初始化完毕
//状态为0则表示初始的节点,有可能还在运行当中,所以不做处理
if (h != null && h.waitStatus != 0)
//唤醒其他线程
//注释2
unparkSuccessor(h);
return true;
}
return false;
}
//注释1 尝试释放锁 releases = 1
protected final boolean tryRelease(int releases) {
//getState方法获取当前重入次数,减1
int c = getState() - releases;
//释放的线程不等于锁独占的线程,则抛异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//如果重入次数为0,则表示可以释放所资源
if (c == 0) {
free = true;
//设置当前独占线程为NULL,允许其他被挂起的资源进行抢占
setExclusiveOwnerThread(null);
}
//如果不为0 则记录剩余的重入次数
setState(c);
return free;
}
//注释2
//唤醒线程
private void unparkSuccessor(Node node) {
//获得锁状态
int ws = node.waitStatus;
if (ws < 0)
//如果小于0 则表示可以被唤醒
//通过CAS操作进行替换状态,成功后status = 0
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
//获取到当前节点的下节点
//如果下节点为空,则说明当前节点是链表的尾部
//如果Status>0 说明改节点是取消状态 所以都可以进入判断内
if (s == null || s.waitStatus > 0) {
s = null;
//循环查找 给t赋值
//t = 尾节点 如果t不为空 也不是当前节点,则t= t的上一个节点
for (Node t = tail; t != null && t != node; t = t.prev)
//如果状态大于等于0,则设置s = t
if (t.waitStatus <= 0)
s = t;
}
//如果下节点不为空,则唤醒该线程
if (s != null)
//注释3
LockSupport.unpark(s.thread);
}
//注释3
//唤醒线程 如果传入的节点线程不为空
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
对比加锁的源码,释放锁的简直是太简单啦,获取到节点之后吧独占状态置空,通过链表查找到可以被唤醒的节点线程,如果有则进行唤醒
看完源码,从加锁到释放锁的流程已经很清晰了,全程都是以CAS操作进行锁抢占,而CAS的操作无处不在也就体现了非公平性的特征,如果有线程释放锁正好有新的线程抢到了锁,则意味着插队成功,而且还通过status判断节点的状态,以及state来判断锁重入的次数,在重入锁中,status的状态仅仅只是冰山一角,还有条件队列等等的没有分析,而在条件队列里status,跟state字段更是用到了极致。
重入锁大致的流程为(理想化流程):
1、ThreadA,抢先进入通过CAS操作获得锁,然后执行lock后的方法
2、ThreadB,慢了一步,通过lock的CAS操作未获得锁,则进入acquire方法中的tryAcquire方法再次抢占锁,如果失败则进入addWaiter方法,初始化等待队列,创建等待节点,此时头节点为空的threadNode尾节点都是ThreadB的(见下图)
3、ThreadB,初始化成功后返回node节点 作为参数传入acquireQueued方法中,开始自旋操作,判断当前节点的头节点是否为空,如果为空则进行锁抢占,再次抢占失败后,则进行线程挂起释放CPU资源,将waitStatus的状态设置为SIGNAL状态
4、ThreadA执行完内部逻辑后,调用unlock方法,判断是否有重入,如果重入次数大于一则重入次数减一,如果重入次数为一则直接释放锁(CAS操作),把当前独占的线程置空。并且唤醒Head节点的下节点线程。
以上就是ReentrantLock的源码浅析与大致的流程讲解,感谢大家耐心观看。