ReentrantLock
首先我们先尝试使用ReentrantLock(下面简称RLock)来感受一下RLock的强大。之后再对其源码进行详细解读
我们首先尝试创建两个线程来调用同一个方法,通过是否使用RLock来体验一下RLock的作用
不使用RLcok的情况
public static void main(String[] args) {
// 我们首先创建两个线程,并且让他们同时调用 doPrint() 方法
Thread t1 = new Thread(Test::doPrint);
t1.setName("t1");
Thread t2 = new Thread(Test::doPrint);
t2.setName("t2");
t1.start();
t2.start();
}
public static void doPrint() {
System.out.println(Thread.currentThread().getName() + "已经被锁住了");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
尝试执行之后会发现,将会同时打印***t1已经被锁住了***和***t2已经被锁住了***。这里虽然将线程睡眠了5s,但是由于是开启了两个线程去跑的,所以这里将会同时打印这两段文字之后再进行睡眠。
使用RLock的情况
尝试使用RLock并再doPrint()中加锁
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(Test::doPrint);
t1.setName("t1");
Thread t2 = new Thread(Test::doPrint);
t2.setName("t2");
t1.start();
t2.start();
}
public static void doPrint() {
lock.lock();
System.out.println(Thread.currentThread().getName() + "已经被锁住了");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
}
这时会发现t1先打印了***t1已经被锁住了***之后过了5秒才打印了***t2已经被锁住了***
这也就是我们经常说的上锁。当线程t1获取锁之后,如果其他线程想获取锁,则需要在等待t1释放锁之后才可以获取到锁。打个比方,没有上锁的代码他就像是一个杂乱的菜市场,所有来买菜的人他就像是一个个的线程,而卖菜的摊主他就像一个cpu,菜市场买菜从不讲究先来后到。假设现在顾客1(线程t1)先来到了菜摊,他左顾右看迟迟不知道想买什么,这时又来了一个顾客2(线程2),他拿了一个白菜就跟摊主(CPU)说,这个白菜我买了哈,钱我转微信给你了,付款完顾客2(线程2)就走了,可是顾客1还在犹豫要买什么菜。这也真实反应了在没上锁的情况下,是不需要讲究先来后到这一说法的。
上了锁的代码就像是一个整齐有序的菜摊(这里指公平锁,至于公平锁和非公平锁的区别下面会讲),每个来买菜的人(线程),都需要排队,讲究先来后到,先来的先得到摊主(CPU)的青睐,慢来的就得老老实实排队等待前面的人接受完服务。在用户角度看来,就是t1先执行了,执行完之后t2才开始执行。但是如果想知道底层到底是怎么实现的,我们需要从源码层面开始剖析
分析加锁过程
还是先拿我们实际生活中的过程举例。假如我们有一个公厕,公厕的门上有一个指示灯,指示灯为红色表示里面有人,指示灯为绿色表示里面没人,一开始卫生间没有人,指示灯为绿色。这时来了第一个客户A,他先看了一下指示灯,发现灯是绿色的,他发现不需要排队,他就开门进去蹲大号了,这时指示灯变为红色。这时有第二个客户B也想过来上厕所,他先看了看指示灯,发现灯是红色的,他就发现自己需要排队等待了,所以他想看了看这个厕所的门前面是不是有其他人,由于他是第二个来上厕所的,所以他没有看他其他人在排队等待,这时客户B就站在了门前第一个位置。客户B等了等有点急,就推了推门暗示厕所里面的人,尝试获得坑位使用权,无奈门从里面反锁尝试失败,这时客户B就会在内心想着,这个人一定是要搞很久才出来了。这时客户B想拿出手机来一把王者荣耀,但是想了想会不会里面的会不会在差不多上完了,就又推了推门尝试获取坑位使用权,结果客户A还是没拉完,客户B索性就开了一把王者慢慢等A上完了。
在现实生活中竞争坑位的过程其实和RLock的上锁过程也是十分相似。
RLock有一个状态值state,这就相当于我们卫生间的指示灯。当线程t1准备获取锁的时候,先判断state是否为0,这就相当于每个客户去上厕所都会先看看指示灯是不是绿色的,如果是绿色的才会进厕所。由于线程t1是第一个来尝试获取锁的,所以他顺利的拿到了锁,并将其state修改为1。这就相当于客户进去之后,指示灯会变为红色的过程。这时线程t2也准备来尝试获取锁,他先判断state是否为0,由于t1还没释放锁所以state的值1,导致线程2没办法马上获得到锁。这是线程t2就会想去开始排队,他首先判断在队列中有没有其他人,也就是相当于客户判断厕所门前是不是有其他人。但是这里有区别的是!线程t2不是将自己放在头节点!而是在头节点放了一个空值,自己放在头节点的下一个位置,来表示厕所里面还有个人,我是第二个使用厕所的人!(这里如果没听懂没关系,待会会结合源码来看和分析)。在形成队列之后线程t2会第二次去尝试获取锁,也就是修改state的值,如果还是得不到锁,那么t2就会将头节点的状态(waitStatus)修改为-1。这就像是客户会试探性的推推门来看看里面的人好了没有,如果里面的人还是没好,那么B就会觉得那里面的人估计要蹲很久了。之后,线程t2在准备park的时候,还会再尝试获取锁,因为park是一个代价比较大的行为,如果能避免则避免,这就像客户B会在准备开王者之前尝试去推推门来尝试获取蹲位所有权,因为开一局王者他是需要很长时间的,需要用户就开始全神贯注傻傻的等,不再做其他事的。只有t2第三次获取不到锁之后,才会真正的被park。
入队过程
在线程t2第一次获取不到锁的时候,开始创建队列
t2先将自己变成一个节点,并判断此时需不需要排队。它发现tail尾指针为空,则确定当前没有人在排队。
创建一个新的空节点,并将头指针和尾指针都指向空节点。
将t2节点的pre设置为tail指向的空节点,并将tail指向t2节点,并将空节点的next指向t2节点
判断当前节点t2的前一个节点是否为头节点,如果是的话第二次尝试获取锁。如果获取不到锁则将头节点的waitStatus状态修改为-1
判断当前节点t2的前一个节点是否为头节点,如果是的话第三次尝试获取锁,并且如果前一个节点的waitStatus为-1,则当前线程park
源码分析
人会说谎,但是代码不会。0就是0,1就是1。如果没有从源码层面来解析RLock,那我上面说的一切都有可能是假的,是错的!所以我们接下来从源码层面来逐行分析RLock。
第一次加锁过程
接着第二节中使用RLock的例子来讲。第一次加锁过程中,由于锁的状态(state)为0,所以线程t1使用CAS修改完state的状态之后,表示上锁成功,则接着执行doPrint()方法中的打印等一系列操作。
先看一张动图,证明我下面图片的所有代码,都是我ctrl+alt+b进去的。里面鼠标的指向,其实就是真实代码执行时候的情况。下面一步步分析
在我们执行了加锁操作之后,就会调用ReentrantLock#lock()方法,接着会根据我们当前的锁是公平锁或者非公平锁来选择进入对应的lock方法(本文讲的都是以公平锁为参照)。
接着会调用FairSync#lock方法
紧接着会先走到acquire中的if判断,if中的tryAcquire(arg)就是尝试获取锁
接下来我们分析这个tryAcquire()方法
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取锁的状态
int c = getState();
// 如果当前锁状态为0,则进入if语句
if (c == 0) {
/*
1. hasQueuedPredecessors():这里首先判断当前队列是否有人在排队,这里由于是第一次进入,所以返回false(下面有这个方法的解析)
由于返回false,再加上取反,所以if判断会接着走
2. compareAndSetState(0, acquires)
这里的acquires是前面传过来的,固定为1
这行代码的意思是使用cas将锁的状态由0修改为1。注意,这是一个原子操作,调用的是unsafe类的方法
结果if都为true,进入if分支
setExclusiveOwnerThread(current)
这个方法的含义是,将当前锁的所有者,标记为当前线程。
因为ReentrantLock是重入锁,所以标记锁的持有者非常有必要!(如果不理解重入锁的可以先不考虑)
最后返回true
*/
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;
}
}
在进入tryAcquire(arg)方法之后,由于第一次进入返回的是true,而当前if分支加了取反,所以不会走if分支,直接返回
前面的代码也是接着返回。最后就会接着执行接下去的代码。这就是完整的第一次加锁过程。
第二次加锁过程
第一次加锁的gif中并没有展示debug,这是因为如果使用了debug,那么t1线程就会卡住,从而t2线程有机可乘先去拿锁并上锁,这也导致了没办法用debug的方式来展示第一次加锁过程。不过我们可以用debug来展示第二次的加锁过程。
由于GIF太大超过上传上限,所以分成两个gif播放
到此,锁竞争过程也通过debug的方式完美的展现出来!