10. ReentrantLock重入锁
juc包下的锁,和 synchronized 相比具有的的特点
- 可中断
- 可以设置超时时间
- 可以设置为公平锁 (先到先得)
- 支持多个条件变量( 具有多个 WaitSet)
- 在对象级别保护临界区(synchronized在jvm级别保护临界区)
- ReentrantLock锁取代了 原先的对象+Monitor锁
与synchronized一样,都支持可重入
// 获取ReentrantLock对象
private ReentrantLock lock = new ReentrantLock();
// 加锁
lock.lock();
try {
// 需要执行的代码
}finally {
// 释放锁
lock.unlock();
}
10.1 可重入
- 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住 ```java @Slf4j(topic = “c.Test12”) public class Test12 { private static ReentrantLock lock= new ReentrantLock();
public static void main(String[] args) {
lock.lock();
try {
log.debug("进入main");
m1();
}finally {
lock.unlock();
}
}
public static void m1(){
lock.lock();
try {
log.debug("进入m1");
m2();
}finally {
lock.unlock();
}
}
public static void m2(){
lock.lock();
try {
log.debug("进入m2");
}finally {
lock.unlock();
}
} }
```java
15:17:00.947 [main] DEBUG c.Test12 - 进入main
15:17:00.950 [main] DEBUG c.Test12 - 进入m1
15:17:00.950 [main] DEBUG c.Test12 - 进入m2
10.2 可打断
- 如果某个线程处于阻塞状态,可以调用其 interrupt 方法让其停止阻塞,获得锁失败简而言之就是:处于阻塞状态的线程,被打断了就不用阻塞了,直接停止运行 。
- 避免死等(被动的死等,需要别的线程帮助),减少死锁发生。
- lockInterruptibly方法才能实现,lock方法不能被打断。
没有被打断
@Slf4j(topic = "c.Test12")
public class Test12 {
private static ReentrantLock lock= new ReentrantLock();
public static void main(String[] args) {
new Thread(()->{
try {
log.debug("尝试获取锁");
// 如果没有竞争,此方法就会获取lock对象的锁
// 如果有竞争,就进入阻塞队列。
// 区别就是可以被其他线程用 interrupt 方法打断
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("被打断了,没有获取到锁");
return;
}
try {
log.debug("获取到锁");
}finally {
lock.unlock();
}
}, "t1").start();
}
}
15:23:01.924 [t1] DEBUG c.Test12 - 尝试获取锁
15:23:01.927 [t1] DEBUG c.Test12 - 获取到锁
被打断了
@Slf4j(topic = "c.Test12")
public class Test12 {
private static ReentrantLock lock= new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
try {
log.debug("尝试获取锁");
// 如果没有竞争,此方法就会获取lock对象的锁
// 如果有竞争,就进入阻塞队列。
// 区别就是可以被其他线程用 interrupt 方法打断
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("被打断了,没有获取到锁,返回");
return;
}
try {
log.debug("获取到锁");
}finally {
lock.unlock();
}
}, "t1");
// 主线程提前获取到锁,让t1进入阻塞队列
lock.lock();
t1.start();
try {
// 睡一秒后打断t1
Thread.sleep(1000);
log.debug("打断t1");
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
15:25:47.668 [t1] DEBUG c.Test12 - 尝试获取锁
15:25:48.667 [main] DEBUG c.Test12 - 打断t1
15:25:48.669 [t1] DEBUG c.Test12 - 被打断了,没有获取到锁,返回
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 com.ll.ch3.Test12.lambda$main$0(Test12.java:18)
at java.lang.Thread.run(Thread.java:748)
10.3 锁超时
- 使用 lock.tryLock 方法会返回获取锁是否成功。如果成功则返回 true ,反之则返回 false 。并且 tryLock 方法可以指定等待时间,参数为:tryLock(long timeout, TimeUnit unit), 其中 timeout 为最长等待时间,TimeUnit 为时间单位
- 简而言之就是:获取锁失败了、获取超时了或者被打断了,不再阻塞,直接停止运行。
- 主动避免死等
不设置等待时间
@Slf4j(topic = "c.Test13")
public class Test13 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
log.debug("尝试获取锁");
if (! lock.tryLock()){
log.debug("获取不到锁");
return;
}
try {
log.debug("获得到锁");
}finally {
lock.unlock();
}
},"t1");
lock.lock();
log.debug("获取到锁");
t1.start();
}
}
15:35:24.703 [main] DEBUG c.Test13 - 获取到锁
15:35:24.706 [t1] DEBUG c.Test13 - 尝试获取锁
15:35:24.706 [t1] DEBUG c.Test13 - 获取不到锁
带参数,设置等待时间
@Slf4j(topic = "c.Test13")
public class Test13 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
log.debug("尝试获取锁");
try {
// 尝试等待1s,如果1s后还得不到锁就返回,支持别的线程打断
if (! lock.tryLock(1, TimeUnit.SECONDS)){
log.debug("获取不到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("被打断了,获取不到锁");
return;
}
try {
log.debug("获得到锁");
}finally {
lock.unlock();
}
},"t1");
lock.lock();
log.debug("获取到锁");
t1.start();
}
}
15:37:46.115 [main] DEBUG c.Test13 - 获取到锁
15:37:46.118 [t1] DEBUG c.Test13 - 尝试获取锁
15:37:47.123 [t1] DEBUG c.Test13 - 获取不到锁
等待了1s,就结束了。
@Slf4j(topic = "c.Test13")
public class Test13 {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(()->{
log.debug("尝试获取锁");
try {
// 尝试等待1s,如果1s后还得不到锁就返回,支持别的线程打断
if (! lock.tryLock(2, TimeUnit.SECONDS)){
log.debug("获取不到锁");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("被打断了,获取不到锁");
return;
}
try {
log.debug("获得到锁");
}finally {
lock.unlock();
}
},"t1");
lock.lock();
log.debug("获取到锁");
t1.start();
try {
Thread.sleep(1000);
lock.unlock();
log.debug("释放了锁");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
15:41:10.443 [main] DEBUG c.Test13 - 获取到锁
15:41:10.449 [t1] DEBUG c.Test13 - 尝试获取锁
15:41:11.451 [main] DEBUG c.Test13 - 释放了锁
15:41:11.451 [t1] DEBUG c.Test13 - 获得到锁
等待2s,然后主线程1s释放了锁,t1就能获得了。
解决哲学家进餐问题
让筷子继承ReentrantLock即可。
@Slf4j(topic = "c.Test11")
public class Test11 {
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("哲学家1", c1, c2).start();
new Philosopher("哲学家2", c2, c3).start();
new Philosopher("哲学家3", c3, c4).start();
new Philosopher("哲学家4", c4, c5).start();
new Philosopher("哲学家5", c5, c1).start();
}
}
// 让筷子继承ReentrantLock
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" +
"name='" + name + '\'' +
'}';
}
}
@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("eat");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
15:48:04.590 [哲学家3] DEBUG c.Philosopher - eat
15:48:05.593 [哲学家3] DEBUG c.Philosopher - eat
15:48:05.593 [哲学家5] DEBUG c.Philosopher - eat
15:48:06.598 [哲学家5] DEBUG c.Philosopher - eat
15:48:06.598 [哲学家3] DEBUG c.Philosopher - eat
15:48:07.602 [哲学家2] DEBUG c.Philosopher - eat
15:48:07.601 [哲学家5] DEBUG c.Philosopher - eat
15:48:08.606 [哲学家5] DEBUG c.Philosopher - eat
15:48:08.606 [哲学家2] DEBUG c.Philosopher - eat
15:48:09.606 [哲学家2] DEBUG c.Philosopher - eat
15:48:09.606 [哲学家4] DEBUG c.Philosopher - eat
15:48:10.607 [哲学家4] DEBUG c.Philosopher - eat
15:48:10.607 [哲学家1] DEBUG c.Philosopher - eat
15:48:11.608 [哲学家1] DEBUG c.Philosopher - eat
15:48:11.608 [哲学家4] DEBUG c.Philosopher - eat
15:48:12.609 [哲学家1] DEBUG c.Philosopher - eat
15:48:12.609 [哲学家3] DEBUG c.Philosopher - eat
15:48:13.610 [哲学家5] DEBUG c.Philosopher - eat
15:48:13.610 [哲学家2] DEBUG c.Philosopher - eat
10.4 公平锁
在线程获取锁失败,进入阻塞队列时,先进入的会在锁被释放后先获得锁。这样的获取方式就是公平的。
ReentrantLock默认不公平
带参数的构造方法:
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
// 默认是不公平锁,需要在创建时指定为公平锁
ReentrantLock lock = new ReentrantLock(true);
10.5 条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入waitSet 等待。
- ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比
- synchronized 使那些不满足条件的线程都在一间休息室等消息
- 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒
- 使用要点:
- await 前需要获得锁
- await 执行后,会释放锁,进入 conditionObject 等待
- await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
- 竞争 lock 锁成功后,从 await 后继续执行(和wait和notify方法类似)
送烟送外卖例子改写
@Slf4j(topic = "c.Test15")
public class Test15 {
private final static Object room = new Object();
private static boolean hasCigarette = false;
private static boolean hasToke = false;
private static ReentrantLock ROOM = new ReentrantLock();
// 等烟休息室
private static Condition waitCigarette = ROOM.newCondition();
// 等外卖休息室
private static Condition waitToke = ROOM.newCondition();
public static void main(String[] args) {
new Thread(() -> {
ROOM.lock();
try {
log.debug("有烟没?{}", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会");
try {
waitCigarette.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小红").start();
new Thread(() -> {
ROOM.lock();
try {
log.debug("外卖送到没?{}", hasToke);
while (!hasToke) {
log.debug("没外卖,先歇会");
try {
//Thread.sleep(2000);
waitToke.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("可以开始干活了");
} finally {
ROOM.unlock();
}
}, "小蓝").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
ROOM.lock();
try {
log.debug("外卖送到了");
hasToke = true;
// 唤醒等外卖的休息室
waitToke.signal();
}finally {
ROOM.unlock();
}
}, "送外卖").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
ROOM.lock();
try {
log.debug("烟送到了");
hasCigarette = true;
// 唤醒等烟的休息室
waitCigarette.signal();
}finally {
ROOM.unlock();
}
}, "送烟").start();
}
}
16:11:02.893 [小红] DEBUG c.Test15 - 有烟没?false
16:11:02.897 [小红] DEBUG c.Test15 - 没烟,先歇会
16:11:02.898 [小蓝] DEBUG c.Test15 - 外卖送到没?false
16:11:02.898 [小蓝] DEBUG c.Test15 - 没外卖,先歇会
16:11:03.893 [送外卖] DEBUG c.Test15 - 外卖送到了
16:11:03.894 [小蓝] DEBUG c.Test15 - 可以开始干活了
16:11:04.896 [送烟] DEBUG c.Test15 - 烟送到了
16:11:04.897 [小红] DEBUG c.Test15 - 可以开始干活了
11. 同步模式之顺序控制
11.1 固定运行顺序
必须先2后1
方法1:wait¬ify
@Slf4j(topic = "c.Test16")
public class Test16 {
private static Object lock = new Object();
// t2是否运行过
private static boolean t2Run = false;
public static void main(String[] args) {
Thread t1 = new Thread(()->{
synchronized (lock){
while (!t2Run){
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("1");
}
},"t1");
Thread t2 = new Thread(()->{
synchronized (lock){
log.debug("2");
t2Run = true;
lock.notify();
}
},"t2");
t1.start();
t2.start();
}
}
16:18:10.164 [t2] DEBUG c.Test16 - 2
16:18:10.168 [t1] DEBUG c.Test16 - 1
方法2:park&unpark ```java @Slf4j(topic = “c.Test17”) public class Test17 { public static void main(String[] args) {
Thread t1 = new Thread(()->{
LockSupport.park();
log.debug("1");
},"t1");
Thread t2 = new Thread(()->{
log.debug("2");
// 让t1恢复运行
LockSupport.unpark(t1);
},"t2");
t1.start();
t2.start();
} }
```java
16:22:32.902 [t2] DEBUG c.Test17 - 2
16:22:32.902 [t1] DEBUG c.Test17 - 1
11.2 交替输出
线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现。
使用wait¬ify ```java @Slf4j(topic = “c.Test18”) public class Test18 {
public static void main(String[] args) {
NotifyWait notifyWait = new NotifyWait(1, 5);
new Thread(()->{
notifyWait.print("a", 1, 2);
},"t1").start();
new Thread(()->{
notifyWait.print("b", 2, 3);
},"t2").start();
new Thread(()->{
notifyWait.print("c", 3, 1);
},"t3").start();
} }
class NotifyWait {
// 等待标记
private int flag;
// 循环次数
private int loop;
public NotifyWait(int flag, int loop) {
this.flag = flag;
this.loop = loop;
}
// 打印
// 打印内容,等待标记,下一个标记
public void print(String str, int waitFlag, int nextFlag){
for (int i = 0; i < loop; i++) {
synchronized (this){
while (flag != waitFlag){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 打印当前值
System.out.print(str);
// 更改标记
flag = nextFlag;
this.notifyAll();
}
}
}
}
```java
abcabcabcabcabc
- 使用wait¬ify ```java
@Slf4j(topic = “c.Test18”) public class Test18 {
public static void main(String[] args) {
AwaitSignal awaitSignal = new AwaitSignal(5);
Condition a = awaitSignal.newCondition();
Condition b = awaitSignal.newCondition();
Condition c = awaitSignal.newCondition();
new Thread(()->{
awaitSignal.print("a", a, b);
},"t1").start();
new Thread(()->{
awaitSignal.print("b", b, c);
},"t2").start();
new Thread(()->{
awaitSignal.print("c", c, a);
},"t3").start();
// 主线程先唤醒a休息室
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
awaitSignal.lock();
try {
a.signal();
}finally {
awaitSignal.unlock();
}
}
}
class AwaitSignal extends ReentrantLock {
// 循环次数
private int loop;
public AwaitSignal(int loop) {
this.loop = loop;
}
// 打印
// 打印内容 ,进入哪个休息室,下一间休息室
public void print(String str, Condition current, Condition next){
for (int i = 0; i < loop; i++) {
lock();
try {
// 等待
current.await();
// 打印自己的
System.out.print(str);
// 叫醒下一个休息室的
next.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
unlock();
}
}
}
}
```java
abcabcabcabcabc
使用park&unpark ```java @Slf4j(topic = “c.Test18”) public class Test18 { public static Thread t1, t2, t3;
public static void main(String[] args) {
ParkAndUnPark obj = new ParkAndUnPark(5);
t1 = new Thread(() -> {
obj.run("a", t2);
});
t2 = new Thread(() -> {
obj.run("b", t3);
});
t3 = new Thread(() -> {
obj.run("c", t1);
});
t1.start();
t2.start();
t3.start();
LockSupport.unpark(t1);
} }
class ParkAndUnPark { public void run(String str, Thread nextThread) { for (int i = 0; i < loopNumber; i++) { LockSupport.park(); System.out.print(str); LockSupport.unpark(nextThread); } }
private int loopNumber;
public ParkAndUnPark(int loopNumber) {
this.loopNumber = loopNumber;
}
}
```java
abcabcabcabcabc
12.本章小结
- 分析多线程访问共享资源时,哪些代码片段属于临界区(对共享资源既有读,又有写)
- 使用 synchronized 互斥解决临界区的线程安全问题
- 掌握 synchronized 锁对象语法
- 掌握 synchronzied 加载成员方法和静态方法语法
- 掌握 wait/notify 同步方法
- 使用 lock(ReentrantLock) 互斥解决临界区的线程安全问题 掌握 lock 的使用细节:可打断、锁超时、公平锁、条件变量
- 学会分析变量的线程安全性、掌握常见线程安全类的使用
- 了解线程活跃性问题:死锁、活锁、饥饿
- 应用方面
- 互斥:使用 synchronized 或 Lock 达到共享资源互斥效果,实现临界区代码的原子性效果,保证线程安全。
- 同步:使用 wait/notify 或 Lock 的条件变量来达到线程间通信效果。
- 原理方面
- monitor、synchronized 、wait/notify 原理
- synchronized 进阶原理
- park & unpark 原理
- 模式方面
- 同步模式之保护性暂停
- 异步模式之生产者消费者
- 同步模式之顺序控制