前言
前些天去面了XXX公司,很菜的我连初面都没过。虽然已经六七年的开发经验了,虽然也带着项目组装模作样的打怪升级。但还是逃不过一些基础问题的连环call。知耻而后勇,打算好好恶补一下。可重入锁是我没有回答好的问题之一,今天就来好好学习一下什么TM叫可重入锁。
问答环节
面试官:请你谈谈可重入锁?
我:嗯~,我用过ReentrantLock。
面试官:具体讲讲呢。
我:。。。
愉快的面试结束了。
什么是可重入锁?
其实大部程序员应该都知道什么是可重入锁,在同一个线程中可以多次获取同一把锁。那只能回答个概念显然是满足不了如今八股文面试的。所以今天就通过文章的形式来让自己比较全面的、具体的认识一下什么是可重入锁。
首先要问自己一个问题,为什么要可重入,可重入的意义是什么?这也就首先要抛出场景。
场景
假如有两个方法,一个方法叫synchronized goHome(),一个方法叫synchronized goToSleep()。可以看到这两个方法都是synchronized修饰的。看下具体代码:
public class SynchronizedDemo {
public synchronized void goHome() {
System.out.println("我已经到家啦,洗洗准备睡了!");
this.goToSleep();
}
public synchronized void goToSleep() {
System.out.println("我睡觉啦,你别过来啊~");
}
public static void main(String[] args) {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
synchronizedDemo.goHome();
}
}
执行结果:
从执行结果可以看出 goToSleep() 方法正常调用了,并没有被阻塞,这也是我们想要的结果。由此可以看出synchronized 也是可重入锁,我在面试中的回答,显然告诉了面试官我有多菜。
以上例子我大概可以明白可重入锁是个啥了。下面再回答一下面试官的问题,具体谈谈什么是可重入锁。
Java中的可重入锁
Java中的synchronized 和 ReentrantLock 都是可重入锁。可重入锁的意义在于防止死锁。本文重点讨论ReentrantLock,synchronized将单独写一遍再做学习。
先看下ReentrantLock的继承关系图:
ReentrantLock实现了Lock接口,对外提供Lock接口的方法。有一个同步器属性,上锁、释放锁都是通过调用同步器的相关方法实现的。构造时,同步器可以选择公平锁/非公平锁,它们都继承了抽象父类Sync,而Sync又继承了AQS。
抽象类AQS维护了等待队列,而ReentrantLock只需要定义共享资源的获取与释放的方式。
可重入功能的实现原理
ReentrantLock的可重入功能基于AQS的同步状态:state。
其原理大致为:当某一线程获取锁后,将state值+1,并记录下当前持有锁的线程,再有线程来获取锁时,判断这个线程与持有锁的线程是否是同一个线程,如果是,将state值再+1,如果不是,阻塞线程。 当线程释放锁时,将state值-1,当state值减为0时,表示当前线程彻底释放了锁,然后将记录当前持有锁的线程的那个字段设置为null,并唤醒其他线程,使其重新竞争锁。
// acquires的值是1
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取state的值
int c = getState();
// 如果state的值等于0,表示当前没有线程持有锁
// 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回true
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// state的值不等于0,表示已经有其他线程持有锁
// 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true
// 如果不是当前线程,获取锁失败,返回false
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
非公平锁的实现原理
因为ReentrantLock同时支持公平锁和非公平锁,所以这里再讲一下非公平锁的实现原理。
ReentrantLock默认无参构造函数使用的是非公平锁,有参构造函数可指定使用公平锁。
对于公平锁:是指在获取锁之前会检查队列中有没有线程在等待,如果有的话就不会去获取锁,而是会入队列。
那么对于非公平锁,显然就是在获取锁之前不会去检查队列中有没有线程在等待,而是直接去获取锁。如果锁没有线程占用,则队列中被唤醒的线程和新来的线程会同时竞争锁。此时,队列中被唤醒的线程并不一定能优先获得锁,所以是非公平的。
非公平得获取锁:
// acquires的值是1
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取state的值
int c = getState();
// 如果state的值等于0,表示当前没有线程持有锁
// 尝试将state的值改为1,如果修改成功,则成功获取锁,并设置当前线程为持有锁的线程,返回true
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// state的值不等于0,表示已经有其他线程持有锁
// 判断当前线程是否等于持有锁的线程,如果等于,将state的值+1,并设置到state上,获取锁成功,返回true
// 如果不是当前线程,获取锁失败,返回false
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平得获取锁:
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 获取锁,与非公平锁的不同的地方在于,这里直接调用的AQS的acquire方法,没有先尝试获取锁
// acquire又调用了下面的tryAcquire方法,核心在于这个方法
final void lock() {
acquire(1);
}
/**
* 这个方法和nonfairTryAcquire方法只有一点不同,在标注为#1的地方
* 多了一个判断hasQueuedPredecessors,这个方法是判断当前AQS的同步队列中是否还有等待的线程
* 如果有,返回true,否则返回false。
* 由此可知,当队列中没有等待的线程时,当前线程才能尝试通过CAS的方式获取锁。
* 否则就让这个线程去队列后面排队。
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// #1
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;
}
}
总结
由于synchronized是基于monitor机制实现的,它只支持非公平锁;但ReentrantLock同时支持公平锁和非公平锁。ReentrantLock同时支持公平锁与非公平锁,支持:尝试非阻塞的一次性获取锁 ,支持超时获取锁,支持可中断的获取锁,支持更多的等待条件(Condition)。 结合了一些文章和自己的理解,整理成为加深自己对知识的理解,可中断获取锁、支持等待条件相关知识没有记录,有不准确的地方欢迎大家批评指正。