剑指JUC原理-7.线程状态与ReentrantLock

  • 👏作者简介:大家好,我是爱吃芝士的土豆倪,24届校招生Java选手,很高兴认识大家
  • 📕系列专栏:Spring源码、JUC源码
  • 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
  • 🍂博主正在努力完成2023计划中:源码溯源,一探究竟
  • 📝联系方式:nhs19990716,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀

文章目录

    • 重新理解线程状态转换
      • 情况 1 NEW --> RUNNABLE
      • 情况 2 RUNNABLE <--> WAITING
      • 情况 3 RUNNABLE <--> WAITING
      • 情况 4 RUNNABLE <--> WAITING
      • 情况 5 RUNNABLE <--> TIMED_WAITING
      • 情况 6 RUNNABLE <--> TIMED_WAITING
      • 情况 7 RUNNABLE <--> TIMED_WAITING
      • 情况 8 RUNNABLE <--> TIMED_WAITING
      • 情况 9 RUNNABLE <--> BLOCKED
      • 情况 10 RUNNABLE <--> TERMINATED
    • 多把锁
      • 多把不相干的锁
    • 活跃性
      • 死锁
        • 定位死锁
          • jps
          • jconsole
      • 哲学家就餐问题
      • 活锁
      • 饥饿
    • ReentrantLock
      • 可重入
      • 可打断
      • 锁超时
      • 公平锁
      • 条件变量
    • 同步模式之顺序控制
      • 固定运行顺序
        • wait notify 版
        • ReentrantLock 版
        • Park Unpark 版
      • 交替输出
        • wait notify 版
        • Lock 条件变量版
        • Park Unpark 版

重新理解线程状态转换

在这里插入图片描述

假设有线程 Thread t

情况 1 NEW --> RUNNABLE

当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况 2 RUNNABLE <–> WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
  • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时

当一个线程被interrupt()方法中断时,它会从WAITING状态转换为RUNNABLE状态,并抛出一个InterruptedException异常。在此之后,它就可以继续执行了,但需要注意的是,在处理InterruptedException异常之前,它必须先清除中断状态,否则可能会导致线程再次进入WAITING状态。

  • 竞争锁成功,t 线程从 WAITING --> RUNNABLE
  • 竞争锁失败,t 线程从 WAITING --> BLOCKED(当线程没办法获得锁的时候,就会进入 block状态)
final static Object obj = new Object();public static void main(String[] args) throws InterruptedException {new Thread(() -> {synchronized (obj) {System.out.println("执行t1....");try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("其它代码....");}},"t1").start();new Thread(() -> {synchronized (obj) {System.out.println("执行t2....");try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}System.out.println("其它代码....");}},"t2").start();Thread.sleep(500);System.out.println("唤醒 obj 上其它线程");synchronized (obj) {obj.notifyAll(); // 唤醒obj上所有等待线程 断点}}

在这里插入图片描述

在这里插入图片描述

可以看到,当发生竞争的时候t1的状态是 Monitor

情况 3 RUNNABLE <–> WAITING

当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING,注意是当前线程在t 线程对象的监视器上等待

t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

情况 4 RUNNABLE <–> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,会让目标线程从 WAITING --> RUNNABLE

情况 5 RUNNABLE <–> TIMED_WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify() , obj.notifyAll() , t.interrupt() 时

竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE

竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

情况 6 RUNNABLE <–> TIMED_WAITING

当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING,注意是当前线程在t 线程对象的监视器上等待

当前线程等待时间超过了 n 毫秒,或t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从
TIMED_WAITING --> RUNNABLE

情况 7 RUNNABLE <–> TIMED_WAITING

当前线程调用 Thread.sleep(long n) ,当前线程从 RUNNABLE --> TIMED_WAITING

当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

情况 8 RUNNABLE <–> TIMED_WAITING

当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线
程从 RUNNABLE --> TIMED_WAITING

调用 LockSupport.unpark(目标线程) 或调用了线程 的 interrupt() ,或是等待超时,会让目标线程从
TIMED_WAITING–> RUNNABLE

情况 9 RUNNABLE <–> BLOCKED

t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED

持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争
成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

情况 10 RUNNABLE <–> TERMINATED

当前线程所有代码运行完毕,进入 TERMINATED

多把锁

多把不相干的锁

一间大屋子有两个功能:睡觉、学习,互不相干。

现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低

解决方法是准备多个房间(多个对象锁)

class BigRoom {public void sleep() {synchronized (this) {log.debug("sleeping 2 小时");Sleeper.sleep(2);}}public void study() {synchronized (this) {log.debug("study 1 小时");Sleeper.sleep(1);}}
}

执行

    	BigRoom bigRoom = new BigRoom();new Thread(() -> {bigRoom.study();},"小南").start();new Thread(() -> {bigRoom.sleep();},"小女").start();

某次结果

12:13:54.471 [小南] c.BigRoom - study 1 小时
12:13:55.476 [小女] c.BigRoom - sleeping 2 小时

时间上可以看出,小女晚了1s执行

改进

class BigRoom {private final Object studyRoom = new Object();private final Object bedRoom = new Object();public void sleep() {synchronized (bedRoom) {log.debug("sleeping 2 小时");Sleeper.sleep(2);}}public void study() {synchronized (studyRoom) {log.debug("study 1 小时");Sleeper.sleep(1);}}
}

某次执行结果

12:15:35.069 [小南] c.BigRoom - study 1 小时
12:15:35.069 [小女] c.BigRoom - sleeping 2 小时

时间上可以看出,是同步执行的。

将锁的粒度细分

  • 好处,是可以增强并发度(锁的细粒度能够提升并发度)
  • 坏处,如果一个线程需要同时获得多把锁,就容易发生死锁

活跃性

线程内的代码本来是有限的,但是由于某种原因,你的线程里面的代码一直执行不完,这就叫做线程的活跃性。

死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁

t1 线程 获得 A对象 锁,接下来想获取 B对象 的锁 t2 线程 获得 B对象 锁,接下来想获取 A对象 的锁

Object A = new Object();Object B = new Object();Thread t1 = new Thread(() -> {synchronized (A) {log.debug("lock A");sleep(1);synchronized (B) {log.debug("lock B");log.debug("操作...");}}}, "t1");Thread t2 = new Thread(() -> {synchronized (B) {log.debug("lock B");sleep(0.5);synchronized (A) {log.debug("lock A");log.debug("操作...");}}}, "t2");t1.start();t2.start();
12:22:06.962 [t2] c.TestDeadLock - lock B 
12:22:06.962 [t1] c.TestDeadLock - lock A 
定位死锁

检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:

jps
cmd > jps
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
12320 Jps
22816 KotlinCompileDaemon
33200 TestDeadLock // JVM 进程
11508 Main
28468 Launcher
cmd > jstack 33200Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-82018-12-29 05:51:40Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b14 mixed mode):"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x0000000003525000 nid=0x2f60 waiting on condition[0x0000000000000000]java.lang.Thread.State: RUNNABLE"Thread-1" #12 prio=5 os_prio=0 tid=0x000000001eb69000 nid=0xd40 waiting for monitor entry[0x000000001f54f000]java.lang.Thread.State: BLOCKED (on object monitor)at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)- waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)- locked <0x000000076b5bf1d0> (a java.lang.Object)at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)at java.lang.Thread.run(Thread.java:745)"Thread-0" #11 prio=5 os_prio=0 tid=0x000000001eb68800 nid=0x1b28 waiting for monitor entry[0x000000001f44f000]java.lang.Thread.State: BLOCKED (on object monitor)at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)- waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)- locked <0x000000076b5bf1c0> (a java.lang.Object)at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)at java.lang.Thread.run(Thread.java:745)// 略去部分输出Found one Java-level deadlock:============================="Thread-1":waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),which is held by "Thread-0""Thread-0":waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),which is held by "Thread-1"Java stack information for the threads listed above:==================================================="Thread-1":at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)- waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)- locked <0x000000076b5bf1d0> (a java.lang.Object)at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)at java.lang.Thread.run(Thread.java:745)"Thread-0":at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)- waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)- locked <0x000000076b5bf1c0> (a java.lang.Object)at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)at java.lang.Thread.run(Thread.java:745)Found 1 deadlock

这里是两个线程 “Thread-0” 和 “Thread-1” 在互相等待对方释放锁,从而导致了死锁。

线程 “Thread-1” 试图获取对象0x000000076b5bf1c0的监视器锁(即synchronized关键字保护的那个对象),但是这个锁已经被线程 “Thread-0” 持有。因此,“Thread-1” 进入了等待状态,等待 “Thread-0” 释放这个锁。

同时,线程 “Thread-0” 试图获取对象0x000000076b5bf1d0的监视器锁,但是这个锁已经被线程 “Thread-1” 持有。因此,“Thread-0” 进入了等待状态,等待 “Thread-1” 释放这个锁。

jconsole

在这里插入图片描述

在这里插入图片描述

  • 避免死锁要注意加锁顺序
  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到
    CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

哲学家就餐问题

在这里插入图片描述

有五位哲学家,围坐在圆桌旁。

他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。

吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。

如果筷子被身边的人拿着,自己就得等待

筷子类

class Chopstick {String name;public Chopstick(String name) {this.name = name;}@Overridepublic String toString() {return "筷子{" + name + '}';}
}

哲学家类

class Philosopher extends Thread {Chopstick left;Chopstick right;public Philosopher(String name, Chopstick left, Chopstick right) {super(name);this.left = left;this.right = right;}private void eat() {log.debug("eating...");Sleeper.sleep(1);}@Overridepublic void run() {while (true) {// 获得左手筷子synchronized (left) {// 获得右手筷子synchronized (right) {// 吃饭eat();}// 放下右手筷子}// 放下左手筷子}}
}

就餐

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();

执行不多会,就执行不下去了

12:33:15.575 [苏格拉底] c.Philosopher - eating... 
12:33:15.575 [亚里士多德] c.Philosopher - eating... 
12:33:16.580 [阿基米德] c.Philosopher - eating... 
12:33:17.580 [阿基米德] c.Philosopher - eating... 
// 卡在这里, 不向下运行

使用 jconsole 检测死锁,发现

在这里插入图片描述

-------------------------------------------------------------------------
名称: 阿基米德
状态: cn.itcast.Chopstick@1540e19d (筷子1) 上的BLOCKED, 拥有者: 苏格拉底
总阻止数: 2, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已锁定 cn.itcast.Chopstick@6d6f6e28 (筷子5)
-------------------------------------------------------------------------
名称: 苏格拉底
状态: cn.itcast.Chopstick@677327b6 (筷子2) 上的BLOCKED, 拥有者: 柏拉图
总阻止数: 2, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已锁定 cn.itcast.Chopstick@1540e19d (筷子1)
-------------------------------------------------------------------------
名称: 柏拉图
状态: cn.itcast.Chopstick@14ae5a5 (筷子3) 上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已锁定 cn.itcast.Chopstick@677327b6 (筷子2)
-------------------------------------------------------------------------
名称: 亚里士多德
状态: cn.itcast.Chopstick@7f31245a (筷子4) 上的BLOCKED, 拥有者: 赫拉克利特
总阻止数: 1, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已锁定 cn.itcast.Chopstick@14ae5a5 (筷子3)
-------------------------------------------------------------------------
名称: 赫拉克利特
状态: cn.itcast.Chopstick@6d6f6e28 (筷子5) 上的BLOCKED, 拥有者: 阿基米德
总阻止数: 2, 总等待数: 0
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)- 已锁定 cn.itcast.Chopstick@7f31245a (筷子4)

这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如

public class TestLiveLock {static volatile int count = 10;static final Object lock = new Object();public static void main(String[] args) {new Thread(() -> {// 期望减到 0 退出循环while (count > 0) {sleep(0.2);count--;log.debug("count: {}", count);}}, "t1").start();new Thread(() -> {// 期望超过 20 退出循环while (count < 20) {sleep(0.2);count++;log.debug("count: {}", count);}}, "t2").start();}
}

开发过程中如果遇到活锁:
让其中执行时间存在一定的交错,或者睡眠的时间是一个随机数

饥饿

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不
易演示,讲读写锁时会涉及饥饿问题

下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题

在这里插入图片描述

顺序加锁的解决方案

在这里插入图片描述

造成线程2 饥饿

ReentrantLock

相对于 synchronized 它具备如下特点

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

与 synchronized 一样,都支持可重入

基本语法

// 获取锁
reentrantLock.lock();
try {// 临界区
} finally {// 释放锁reentrantLock.unlock();
}

可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁

如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

	static ReentrantLock lock = new ReentrantLock();public static void main(String[] args) {method1();}public static void method1() {lock.lock();try {log.debug("execute method1");method2();} finally {lock.unlock();}}public static void method2() {lock.lock();try {log.debug("execute method2");method3();} finally {lock.unlock();}}public static void method3() {lock.lock();try {log.debug("execute method3");} finally {lock.unlock();}}

输出

17:59:11.862 [main] c.TestReentrant - execute method1 
17:59:11.865 [main] c.TestReentrant - execute method2 
17:59:11.865 [main] c.TestReentrant - execute method3 

可打断

ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("启动...");try {lock.lockInterruptibly(); // 上锁,这是可打断的。// 如果没有竞争,那么此方法就会获取lock对象锁// 如果有竞争,就进入到阻塞队列,也就是阻塞状态,可以被其他线程用interrupt方法打断,并抛出异常} catch (InterruptedException e) {e.printStackTrace();log.debug("等锁的过程中被打断");return;}try {log.debug("获得了锁");} finally {lock.unlock();}}, "t1");}lock.lock(); // main线程上锁,此时 lock.lockInterruptibly  就阻塞了log.debug("获得了锁");t1.start();try {sleep(1);t1.interrupt();log.debug("执行打断");} finally {lock.unlock();}

输出

18:02:40.520 [main] c.TestInterrupt - 获得了锁
18:02:40.524 [t1] c.TestInterrupt - 启动... 
18:02:41.530 [main] c.TestInterrupt - 执行打断
java.lang.InterruptedException at 
java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchr
onizer.java:898) at 
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchron
izer.java:1222) at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) at cn.itcast.n4.reentrant.TestInterrupt.lambda$main$0(TestInterrupt.java:17) at java.lang.Thread.run(Thread.java:748) 
18:02:41.532 [t1] c.TestInterrupt - 等锁的过程中被打断

注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断

ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("启动...");lock.lock();try {log.debug("获得了锁");} finally {lock.unlock();}}, "t1");lock.lock();log.debug("获得了锁");t1.start();try {sleep(1);t1.interrupt();log.debug("执行打断");sleep(1);} finally {log.debug("释放了锁");lock.unlock();}

输出

18:06:56.261 [main] c.TestInterrupt - 获得了锁
18:06:56.265 [t1] c.TestInterrupt - 启动... 
18:06:57.266 [main] c.TestInterrupt - 执行打断 // 这时 t1 并没有被真正打断, 而是仍继续等待锁
18:06:58.267 [main] c.TestInterrupt - 释放了锁
18:06:58.267 [t1] c.TestInterrupt - 获得了锁

可打断是一种被动的 打断方法,由其他线程调用 interrupt才可以。

锁超时

立刻失败

ReentrantLock lock = new ReentrantLock();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();try {sleep(2);} finally {lock.unlock();}

输出:

18:15:02.918 [main] c.TestTimeout - 获得了锁
18:15:02.921 [t1] c.TestTimeout - 启动... 
18:15:02.921 [t1] c.TestTimeout - 获取立刻失败,返回

超时失败

 ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("启动...");try {if (!lock.tryLock(1, TimeUnit.SECONDS)) {log.debug("获取等待 1s 后失败,返回");return;}} catch (InterruptedException e) {e.printStackTrace();}try {log.debug("获得了锁");} finally {lock.unlock();}}, "t1");
lock.lock();log.debug("获得了锁");t1.start();try {sleep(2);} finally {lock.unlock();}

输出

18:19:40.537 [main] c.TestTimeout - 获得了锁
18:19:40.544 [t1] c.TestTimeout - 启动... 
18:19:41.547 [t1] c.TestTimeout - 获取等待 1s 后失败,返回

使用 tryLock 解决哲学家就餐问题

class Chopstick extends ReentrantLock {String name;public Chopstick(String name) {this.name = name;}@Overridepublic String toString() {return "筷子{" + name + '}';}
}class Philosopher extends Thread {Chopstick left;Chopstick right;public Philosopher(String name, Chopstick left, Chopstick right) {super(name);this.left = left;this.right = right;}@Overridepublic 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 默认是不公平的

ReentrantLock lock = new ReentrantLock(false);
lock.lock();for (int i = 0; i < 500; i++) {new Thread(() -> {lock.lock();try {System.out.println(Thread.currentThread().getName() + " running...");} finally {lock.unlock();}}, "t" + i).start();}
// 1s 之后去争抢锁Thread.sleep(1000);new Thread(() -> {System.out.println(Thread.currentThread().getName() + " start...");lock.lock();try {System.out.println(Thread.currentThread().getName() + " running...");} finally {lock.unlock();}}, "强行插入").start();lock.unlock();

强行插入,有机会在中间输出。注意:该实验不一定总能复现

t39 running... 
t40 running... 
t41 running... 
t42 running... 
t43 running... 
强行插入 start... 
强行插入 running... 
t44 running... 
t45 running... 
t46 running... 
t47 running... 
t49 running... 

改为公平锁后(本意是为了解决饥饿问题的)

ReentrantLock lock = new ReentrantLock(true);

强行插入,总是在最后输出

t465 running... 
t464 running... 
t477 running... 
t442 running... 
t468 running... 
t493 running... 
t482 running... 
t485 running... 
t481 running... 
强行插入 running... 

条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行

例子:

static ReentrantLock lock = new ReentrantLock();static Condition waitCigaretteQueue = lock.newCondition();static Condition waitbreakfastQueue = lock.newCondition();static volatile boolean hasCigrette = false;static volatile boolean hasBreakfast = false;public static void main(String[] args) {new Thread(() -> {try {lock.lock();while (!hasCigrette) {try {waitCigaretteQueue.await();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("等到了它的烟");} finally {lock.unlock();}}).start();new Thread(() -> {try {lock.lock();while (!hasBreakfast) {try {waitbreakfastQueue.await();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("等到了它的早餐");} finally {lock.unlock();}}).start();sleep(1);sendBreakfast();sleep(1);sendCigarette();}private static void sendCigarette() {lock.lock();try {log.debug("送烟来了");hasCigrette = true;waitCigaretteQueue.signal();} finally {lock.unlock();}}private static void sendBreakfast() {lock.lock();try {log.debug("送早餐来了");hasBreakfast = true;waitbreakfastQueue.signal();} finally {lock.unlock();}}

输出

18:52:27.680 [main] c.TestCondition - 送早餐来了
18:52:27.682 [Thread-1] c.TestCondition - 等到了它的早餐
18:52:28.683 [main] c.TestCondition - 送烟来了
18:52:28.683 [Thread-0] c.TestCondition - 等到了它的烟

同步模式之顺序控制

固定运行顺序

比如,必须先 2 后 1 打印

wait notify 版
// 用来同步的对象static Object obj = new Object();// t2 运行标记, 代表 t2 是否执行过static boolean t2runed = false;public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (obj) {// 如果 t2 没有执行过while (!t2runed) {try {// t1 先等一会obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}}System.out.println(1);});Thread t2 = new Thread(() -> {System.out.println(2);synchronized (obj) {// 修改运行标记t2runed = true;// 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)obj.notifyAll();}});t1.start();t2.start();}
ReentrantLock 版
public class Main {static ReentrantLock lock = new ReentrantLock();static Condition condition = lock.newCondition();static boolean t2runed = false;public static void main(String[] args) {Thread t1 = new Thread(() -> {lock.lock();try {while (!t2runed) {try {condition.await();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(1);} finally {lock.unlock();}});Thread t2 = new Thread(() -> {System.out.println(2);lock.lock();try {t2runed = true;condition.signalAll();} finally {lock.unlock();}});t1.start();t2.start();}
}
Park Unpark 版

可以看到,实现上很麻烦:

  • 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该wait
  • 第二,如果有些干扰线程错误地 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题
  • 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个

可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目:

    Thread t1 = new Thread(() -> {try { Thread.sleep(1000); } catch (InterruptedException e) { }// 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行LockSupport.park();System.out.println("1");});Thread t2 = new Thread(() -> {System.out.println("2");// 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)LockSupport.unpark(t1);});t1.start();t2.start();

park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『复』,不需要『同步对象』和『运行标记』

交替输出

线程 1 输出 a 5 次,线程 2 输出 b 5 次,线程 3 输出 c 5 次。现在要求输出 abcabcabcabcabc 怎么实现

wait notify 版
class SyncWaitNotify {private int flag;private int loopNumber;public SyncWaitNotify(int flag, int loopNumber) {this.flag = flag;this.loopNumber = loopNumber;}public void print(int waitFlag, int nextFlag, String str) {for (int i = 0; i < loopNumber; i++) {synchronized (this) {while (this.flag != waitFlag) {try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.print(str);flag = nextFlag;this.notifyAll();}}}
}
SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
new Thread(() -> {syncWaitNotify.print(1, 2, "a");
}).start();
new Thread(() -> {syncWaitNotify.print(2, 3, "b");
}).start();
new Thread(() -> {syncWaitNotify.print(3, 1, "c");
}).start();

在这里详细解读一下,说一下主体的流程,一开始三个线程同时执行 print,线程同时启动,首先争取锁,先假设一个最理想的情况就是syncWaitNotify.print(1, 2, “a”);这个线程争取到锁了,此时while不成立,不用等待,输出,然后设置flag,然后紧接着syncWaitNotify.print(2, 3, “b”);获取到锁了,然后继续syncWaitNotify.print(3, 1, “c”);获取到锁啦,依次执行即可。

但是线程之间的执行顺序是不可预测的,下面随便举例子,比如syncWaitNotify.print(2, 3, “b”);先获取到锁啦,但是满足了while条件,进入等待,此时算是释放了锁,然后syncWaitNotify.print(3, 1, “c”);获取到锁啦,继续进入while条件,进入等待,又算是释放了锁,最后进入syncWaitNotify.print(1, 2, “a”);,执行并唤醒了所有wait,此时flag设置为了2,假如此时syncWaitNotify.print(3, 1, “c”);获取到锁啦,继续进入while条件,有算是释放了锁,此时syncWaitNotify.print(2, 3, “b”);获取到锁啦,输出并唤醒syncWaitNotify.print(3, 1, “c”);,然后syncWaitNotify.print(3, 1, “c”);获得了锁,执行完abc流程。

Lock 条件变量版
class AwaitSignal extends ReentrantLock {public void start(Condition first) {this.lock();try {log.debug("start");first.signal();} finally {this.unlock();}}public void print(String str, Condition current, Condition next) {for (int i = 0; i < loopNumber; i++) {this.lock();try {current.await();log.debug(str);next.signal();} catch (InterruptedException e) {e.printStackTrace();} finally {this.unlock();}}}// 循环次数private int loopNumber;public AwaitSignal(int loopNumber) {this.loopNumber = loopNumber;}
}AwaitSignal as = new AwaitSignal(5);Condition aWaitSet = as.newCondition();Condition bWaitSet = as.newCondition();Condition cWaitSet = as.newCondition();
new Thread(() -> {as.print("a", aWaitSet, bWaitSet);}).start();new Thread(() -> {as.print("b", bWaitSet, cWaitSet);}).start();new Thread(() -> {as.print("c", cWaitSet, aWaitSet);}).start();as.start(aWaitSet);

这个实际的场景是这样的,首先每个线程都执行print方法,都分别获取到锁,然后都进入了await(),首先执行start方法,唤醒a的条件变量,输出,然后继续唤醒b的条件变量,按照这个顺序即可实现。

Park Unpark 版
class SyncPark {private int loopNumber;private Thread[] threads;public SyncPark(int loopNumber) {this.loopNumber = loopNumber;}public void setThreads(Thread... threads) {this.threads = threads;}public void print(String str) {for (int i = 0; i < loopNumber; i++) {LockSupport.park();System.out.print(str);LockSupport.unpark(nextThread());}}private Thread nextThread() {Thread current = Thread.currentThread();int index = 0;for (int i = 0; i < threads.length; i++) {if(threads[i] == current) {index = i;break;}}if(index < threads.length - 1) {return threads[index+1];} else {return threads[0];}}public void start() {for (Thread thread : threads) {thread.start();}LockSupport.unpark(threads[0]);}
}SyncPark syncPark = new SyncPark(5);
Thread t1 = new Thread(() -> {syncPark.print("a");
});
Thread t2 = new Thread(() -> {syncPark.print("b");
});
Thread t3 = new Thread(() -> {syncPark.print("c\n");
});
syncPark.setThreads(t1, t2, t3);
syncPark.start();

和lock的情况差不多。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/155489.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

信号类型(通信)——QPSK、OQPSK、IJF_OQPSK调制信号

系列文章目录 《信号类型&#xff08;通信&#xff09;——仿真》 《信号类型&#xff08;通信&#xff09;——QAM调制信号》 文章目录 前言 一、QPSK通信调制信号 1.1、原理 1.2、仿真 二、OQPSK通信调制信号 1.1、原理 1.2、仿真 三、IJF_OQPSK通信调制信号 1.1、…

二蛋赠书六期:《Linux管理入门经典(第8版)》

前言 大家好&#xff01;我是二蛋&#xff0c;一个热爱技术、乐于分享的工程师。在过去的几年里&#xff0c;我一直通过各种渠道与大家分享技术知识和经验。我深知&#xff0c;每一位技术人员都对自己的技能提升和职业发展有着热切的期待。因此&#xff0c;我非常感激大家一直…

钢琴培训答题服务预约小程序的效果怎样

很多家长都会从小培养孩子的兴趣&#xff0c;钢琴便是其中热度较高的一种&#xff0c;而各城市也不乏线下教育培训机构&#xff0c;除了青少年也有成年人参加培训&#xff0c;市场教育高需求下&#xff0c;需要商家不断拓展客户和转化。 那么通过【雨科】平台制作钢琴培训服务…

Python与Pycharm的安装与使用

1. Python下载与安装 python官网&#xff1a;链接 Python下载&#xff1a;各版本下载链接 1.1 Python下载 1、首先进入下载页面&#xff0c;选择一个版本下载&#xff0c;这里以Python3.7为例 2、点击下载&#xff0c;随后得到对应安装包 1.2 Python安装 1、双击进行软件…

什么是Web 3.0?

什么是Web 3.0&#xff1f;简而言之&#xff0c;就是第三代互联网。 在回答Web 3.0之前&#xff0c;让我们先看一下Web 1.0和Web 2.0。 互联网革命 Web 1.0&#xff0c;第一代互联网&#xff0c;从互联网诞生到1997年。 在Web 1.0&#xff0c;互联网的信息是静态的只读网页&a…

7+共病思路。WGCNA+多机器学习+实验简单验证,易操作

今天给同学们分享一篇共病WGCNA多机器学习实验的生信文章“Shared diagnostic genes and potential mechanism between PCOS and recurrent implantation failure revealed by integrated transcriptomic analysis and machine learning”&#xff0c;这篇文章于2023年5月16日发…

处理大数据的基础架构,OLTP和OLAP的区别,数据库与Hadoop、Spark、Hive和Flink大数据技术

处理大数据的基础架构&#xff0c;OLTP和OLAP的区别&#xff0c;数据库与Hadoop、Spark、Hive和Flink大数据技术 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&am…

KnowledgeGPT:利用检索和存储访问知识库上增强大型语言模型10.30

利用检索和存储访问知识库上增强大型语言模型 摘要引言2 相关研究3方法3.1 任务定义3.2 知识检索3.2.1 代码实现3.2.2 实体链接3.2.3 获取实体信息3.2.4 查找实体或值3.2.5 查找关系 3.3 知识存储 4 实验 摘要 大型语言模型&#xff08;LLM&#xff09;在自然语言处理领域展现…

(四)docker:为mysql和java jar运行环境创建同一网络,容器互联

看了很多资料&#xff0c;说做互联的一个原因是容器内ip不固定&#xff0c;关掉重启后如果有别的容器启动&#xff0c;之前的ip会被占用&#xff0c;所以做互联创建一个网络&#xff0c;让几个容器处于同一个网络&#xff0c;就可以互联还不受关闭再启动ip会改变的影响&#xf…

Android问题笔记四十二:signal 11 (SIGSEGV), code 1 (SEGV_MAPERR) 的解决方法

点击跳转>Unity3D特效百例点击跳转>案例项目实战源码点击跳转>游戏脚本-辅助自动化点击跳转>Android控件全解手册点击跳转>Scratch编程案例点击跳转>软考全系列 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&…

人工智能基础_机器学习008_使用正规方程_损失函数进行计算_一元一次和二元一次方程演示_sklearn线性回归演示---人工智能工作笔记0048

自然界很多都是正态分布的,身高,年龄,体重...但是财富不是. 然后我们来看一下这个y = wx+b 线性回归方程. 然后我们用上面的代码演示. 可以看到首先import numpy as np 导入numby 数据计算库 import matplotlib.pyplot as plt 然后导入图形画的库 然后: X = np.linspace(0,…

Spring cloud教程Gateway服务网关

Spring cloud教程|Gateway服务网关 写在前面的话&#xff1a; 本笔记在参考网上视频以及博客的基础上&#xff0c;只做个人学习笔记&#xff0c;如有侵权&#xff0c;请联系删除&#xff0c;谢谢&#xff01; Spring Cloud Gateway 是 Spring Cloud 的一个全新项目&#xff0c;…