都支持可重入,相对于 synchronized 它还具备如下特点
基本语法
// 获取锁(也可以放在try里面)
// lock 和 unlock 之间代码的都可以看作临界区
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
与 synchronized 一样,都支持可重入。可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。
例如:
@Slf4j(topic = "c.Test22_1")
public class Test22_1 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
log.debug("enter main");
m1();
} finally {
lock.unlock();
}
}
private static void m1() {
lock.lock();
try {
log.debug("enter m1");
m2();
} finally {
lock.unlock();
}
}
private static void m2() {
lock.lock();
try {
log.debug("enter m2");
} finally {
lock.unlock();
}
}
}
/*Output:
17:38:41.749 c.Test22_1 [main] - enter main
17:38:41.759 c.Test22_1 [main] - enter m1
17:38:41.760 c.Test22_1 [main] - enter m2
*/
可打断指的是当线程竞争锁失败时,线程会进入阻塞状态(BLOCKED
)等待锁释放
synchronized
则不能被打断,会一直阻塞;.lockInterruptibly()
方法加锁,则可以打断处于阻塞状态的线程,打断后会抛出异常InterruptedException
。注意,获得锁之后都可以被打断。
示例:
@Slf4j(topic = "c.Test22_2")
public class Test22_2 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
// 如果没有竞争 --> 获取lock对象锁
// 如果有竞争 --> 进入阻塞队列,但是可以被其他线程打断,不用继续等
// 注意是在阻塞的时候被打断,即还没获得锁,不是在获得锁之后被打断
log.debug("尝试获得锁");
lock.lockInterruptibly(); // 可被打断锁
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("等锁的过程中被打断,没有获得到锁,返回");
// 如果不返回会继续执行完本线程,这违背了锁的初衷
return;
}
try {
log.debug("获取到锁");
} finally {
lock.unlock();
}
}, "t1");
// 主线程先加锁
lock.lock();
t1.start();
Sleeper.sleep(1);
log.debug("打断t1");
t1.interrupt();
}
}
/*Output:
18:46:44.618 c.Test22_2 [t1] - 尝试获得锁
18:46:45.618 c.Test22_2 [main] - 打断t1
18:46:45.621 c.Test22_2 [t1] - 等锁的过程中被打断,没有获得到锁,返回
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at cn.itcast.test.Test22_2.lambda$main$0(Test22_2.java:21)
at java.lang.Thread.run(Thread.java:748)
*/
如果用.lock
的话打断后没有任何反应,还会一直阻塞。
在获取锁的过程中,如果其他线程持有着锁一直没有释放,尝试获取锁的线程不会一直阻塞,会立即(tryLock()
)或者等待一段时间(tryLock(long timeout, TimeUnit unit)
)放弃,如果在这段时间后仍然没有获得锁,会取消阻塞。
源码:
// 返回一个 Boolean 值,获取到锁则返回 true
// 可以看出这个方法也可以打断
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
例子:
@Slf4j(topic = "c.Test22")
public class Test22 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("t1尝试获得锁");
try {
// 等待2s
if (! lock.tryLock(2, TimeUnit.SECONDS)) {
log.debug("t1获取不到锁");
// 获取不到锁,应该返回
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("被打断,t1获取不到锁");
return;
}
try {
log.debug("t1获得到锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("主线程获得到锁");
t1.start();
// 睡眠1s
sleep(1);
log.debug("主线程释放了锁");
lock.unlock();
}
}
/*Output:
20:26:44.061 c.Test22 [main] - 主线程获得到锁
20:26:44.067 c.Test22 [t1] - t1尝试获得锁
20:26:45.067 c.Test22 [main] - 主线程释放了锁
20:26:45.067 c.Test22 [t1] - t1获得到锁
*/
解决哲学家思考问题
:
public class TestDeadLock {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c5, c1).start();
}
}
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子,获取失败会立即放弃
if (left.tryLock()) {
try {
// 尝试获得右手筷子,获取失败会立即放弃,继续往下执行,会释放左手锁
if (right.tryLock()) {
try {
eat();
} finally {
right.unlock();
}
}
} finally {
left.unlock();
}
}
}
}
private void eat() {
log.debug("eating...");
Sleeper.sleep(1);
}
}
// 继承 ReentrantLock,成为锁对象
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
/*Output:
不会产生死锁
*/
公平锁的意思就是先到先执行,本义是为了解决饥饿问题
,但是用的不多,因为tryLock
就可以解决饥饿问题
,而且设置为公平锁会降低并发度。
synchronized
是不公平的,ReentrantLock
默认也是不公平的,但可以设置成公平锁:
// 默认不开启公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// 可以自行设置开启公平锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
源码角度分析
synchronized
中也有条件变量,就是我们讲原理时那个 WaitSet
休息室,当条件不满足时进入 WaitSet
等待;ReentrantLock
的条件变量比 synchronized
强大之处在于,它是支持多个条件变量的,这就好比
synchronized
是那些不满足条件的线程都在一间休息室等消息ReentrantLock
支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒使用要点:
conditionObject
等待使用方法:
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
// 创建一个新的条件变量(休息室)
Condition condition1 = lock.newCondition();
Condition condition2 = lock.newCondition();
// 获得锁
lock.lock();
// 进入休息室1等待
condition1.await();
// 唤醒休息室1里的一个线程
condition1.signal();
// 唤醒休息室1里的所有线程
condition1.signalAll();
}
示例:
@Slf4j(topic = "c.Test24")
public class Test24 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
// 锁对象
static ReentrantLock ROOM = new ReentrantLock();
// 等待烟的休息室(条件变量1)
static Condition waitCigaretteSet = ROOM.newCondition();
// 等外卖的休息室(条件变量2)
static Condition waitTakeoutSet = ROOM.newCondition();
public static void main(String[] args) {
new Thread(() -> {
ROOM.lock();
try {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
// 去等待烟的休息室等待
waitCigaretteSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小南").start();
new Thread(() -> {
ROOM.lock();
try {
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
// 去等待外卖的休息室等待
waitTakeoutSet.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小女").start();
sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasTakeout = true;
// 相比 synchronized, 唤醒的范围缩小
log.debug("外卖到了!");
waitTakeoutSet.signal();
} finally {
ROOM.unlock();
}
}, "送外卖的").start();
sleep(1);
new Thread(() -> {
ROOM.lock();
try {
hasCigarette = true;
// 相比 synchronized, 唤醒的范围缩小
log.debug("烟到了!");
waitCigaretteSet.signal();
} finally {
ROOM.unlock();
}
}, "送烟的").start();
}
}
/*Output:
22:40:42.005 c.Test24 [小南] - 有烟没?[false]
22:40:42.011 c.Test24 [小南] - 没烟,先歇会!
22:40:42.013 c.Test24 [小女] - 外卖送到没?[false]
22:40:42.013 c.Test24 [小女] - 没外卖,先歇会!
22:40:43.005 c.Test24 [送外卖的] - 外卖到了!
22:40:43.006 c.Test24 [小女] - 可以开始干活了
22:40:44.032 c.Test24 [送烟的] - 烟到了!
22:40:44.032 c.Test24 [小南] - 可以开始干活了
*/
以上介绍了Reentrantlock
相比synchronized
重要的四个特点以及都具有的可重入特性,在此想谈一下对这两种锁机制的思考。
synchronized
的临界区(加锁的作用域)为代码块,所以执行完代码块会自动释放锁;而Reentrantlock
需要用lock
、unlock
方法手动加锁、释放锁。synchronized
还是Reentrantlock
都维护着一段临界代码,在synchronized
中获取不到锁就一直阻塞,临界区代码就不会被执行,不用考虑后续的问题;而Reentrantlock
提供了打断和超时等选项,所以尤其要注意,当获取锁失败后(例如阻塞时打断或超时)一定要记得return
,否则临界区代码有可能会继续执行,这违背了锁的初衷。synchronized
是对某一个对象加锁,释放锁只能等临界区代码执行完或进入WaitSet
,而且不能被打断;Reentrantlock
是对Reentrantlock
对象加锁,所以Reentrantlock
释放锁比较方便,可以在任意线程里调用unlock
方法,而且提供了可被打断的锁实现方法.lockInterruptibly()
,可打断这种机制也能解决死锁问题,但这是一种被动的解决死锁的方案。synchronized
获取锁失败一定会阻塞住,即进入加锁对象的EntrySet
等待,这样容易产生死锁;而Reentrantlock
提供了超时方法trylock
, 这是一种主动的解决死锁的方案。synchronized
在对对象加锁后,关联的Monitor
中只有一个WaitSet
,而Reentrantlock
可以新建多个,这样更易于管理,唤醒的精度更高。