当前位置: 首页 > news >正文

java并发入门(一)共享模型—Synchronized、Wait/Notify、pack/unpack

一、共享模型—管程

1、共享存在的问题

1.1 共享变量案例

package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;@Slf4j(topic = "c.MTest1")
public class MTest1 {static int counter = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {counter++;}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {counter--;}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("{}",counter);}
}

以上的结果可能是正数、负数、零。这是因为 Java 中对静态变量的自增,自减并不是原子操作。

例如对于 i++ 而言(i 为静态变量),实际会产生如下的 JVM 字节码指令:

getstatic i   // 获取静态变量i的值
iconst_1      // 准备常量1
iadd          // 自增
putstatic i   // 将修改后的值存入静态变量i

i–会产生如下的 JVM 字节码指令:

getstatic i      // 获取静态变量i的值
iconst_1         // 准备常量1
isub             // 自减
putstatic i      // 将修改后的值存入静态变量i

在Java中,完成静态变量的自增,自减需要在主存和工作内存中进行数据交换 。

在这里插入图片描述

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

在这里插入图片描述

但多线程下这 8 行代码可能交错运行,比如出现负数情况如下:

在这里插入图片描述

1.2 临界区

一个程序运行多个线程本身是没有问题的,问题出在多个线程访问**共享资源

多个线程读共享资源其实也没有问题
在多个线程对
共享资源读写操作时发生指令交错,就会出现问题。一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区**

static int counter = 0;static void increment()
// 临界区
{counter++;
}static void decrement()
// 临界区
{counter--;
}

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

2、利用synchronized 解决共享存在的问题

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock

  • 非阻塞式的解决方案:原子变量

synchronized,即【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。

// 语法如下
synchronized(对象) // 线程1, 线程2(blocked)
{临界区
}

上诉代码,可以进行改造:

package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;@Slf4j(topic = "c.MTest2")
public class MTest2 {static int counter = 0;static final Object room = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (room){counter++;}}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {synchronized (room){counter--;}}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("{}",counter);}
}
  • synchronized(对象) 中的对象,可以想象为一个房间(room),有唯一入口(门)房间只能一次进入一人进行计算,线程 t1,t2 想象成两个人

  • 当线程 t1 执行到 synchronized(room) 时就好比 t1 进入了这个房间,并锁住了门拿走了钥匙,在门内执行count++ 代码

  • 这时候如果 t2 也运行到了 synchronized(room) 时,它发现门被锁住了,只能在门外等待,发生了上下文切换,阻塞住了

  • 这中间即使 t1 的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,t1 仍拿着钥匙,t2 线程还在阻塞状态进不来,只有下次轮到 t1 自己再次获得时间片时才能开门进入

  • 当 t1 执行完 synchronized{} 块内的代码,这时候才会从 obj 房间出来并解开门上的锁,唤醒 t2 线程把钥匙给他。t2 线程这时才可以进入 obj 房间,锁住了门拿上钥匙,执行它的 count-- 代码

如果把 synchronized(obj) 放在 for 循环的外面,如何理解?-- 原子性
如果 t1 synchronized(obj1) 而 t2 synchronized(obj2) 会怎样运作?-- 锁对象
如果 t1 synchronized(obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象

利用面向对象的思想,将共享变量放入到一个类中,将上述代码改造如下:

package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;@Slf4j(topic = "c.MTest3")
public class MTest3 {public static void main(String[] args) throws InterruptedException {RoomCounter counter = new RoomCounter();Thread t1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {counter.increment();}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {counter.decrement();}}, "t2");t1.start();t2.start();t1.join();t2.join();log.debug("{}",counter);}
}class RoomCounter{int value = 0;public void increment() {synchronized (this) {value++;}}public void decrement() {synchronized (this) {value--;}}public int get() {synchronized (this) {return value;}}}

3、方法上的synchronized

// 普通方法
class Test{public synchronized void test() {}
}// 等价于  此时锁住的是调用者
class Test{public void test() {synchronized(this) {}}
}
// 静态方法
class Test{public synchronized static void test() {}
}
// 等价于  此时锁住的是Test类
class Test{public static void test() {synchronized(Test.class) {}}
}

4、变量安全性分析

4.1 基本概念

成员变量和静态变量是否线程安全?

  • 如果它们没有共享,则线程安全

  • 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况

    • 如果只有读操作,则线程安全

    • 如果有读写操作,则这段代码是临界区,需要考虑线程安全

package com.yyds.juc.monitor;import java.util.ArrayList;public class MTest4ThreadUnsafe {ArrayList<String> list = new ArrayList<>();static final int THREAD_NUMBER = 2;static final int LOOP_NUMBER = 200;public static void main(String[] args) {// 如果线程2 还未 add,线程1 remove 就会报错MTest4ThreadUnsafe test = new MTest4ThreadUnsafe();for (int i = 0; i < THREAD_NUMBER; i++) {new Thread(() -> {test.method1(LOOP_NUMBER);}, "Thread" + i).start();}}public void method1(int loopNumber){for (int i = 0; i < loopNumber; i++) {// 临界区,会产生竞态条件method2();method3();}}private void method2() {list.add("1");}private void method3() {list.remove(0);}}

将成员变量改为局部变量,不会报错,代码如下

package com.yyds.juc.monitor;import java.util.ArrayList;public class MTest5ThreadSafe {static final int THREAD_NUMBER = 2;static final int LOOP_NUMBER = 200;public static void main(String[] args) {// list 是局部变量,每个线程调用时会创建其不同实例,没有共享// 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象// method3 的参数分析与 method2 相同MTest4ThreadUnsafe test = new MTest4ThreadUnsafe();for (int i = 0; i < THREAD_NUMBER; i++) {new Thread(() -> {test.method1(LOOP_NUMBER);}, "Thread" + i).start();}}public void method1(int loopNumber){ArrayList<String> list = new ArrayList<>();for (int i = 0; i < loopNumber; i++) {// 临界区,会产生竞态条件method2(list);method3(list);}}private void method2(ArrayList<String> list) {list.add("1");}private void method3(ArrayList<String> list) {list.remove(0);}
}

局部变量是否线程安全?

  • 局部变量是线程安全的

  • 但局部变量引用的对象则未必

    • 如果该对象没有逃离方法的作用访问,它是线程安全的

    • 如果该对象逃离方法的作用范围,需要考虑线程安全

// 局部变量是线程安全的
public static void test1() {int i = 10;i++;
}
// 如下图,每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享

在这里插入图片描述

在上面例子中,将成员变量改为局部变量,不报错了,但是如果出现下面对象逃离方法的作用范围情况,依旧报错

package com.yyds.juc.monitor;import java.util.ArrayList;public class MTest5ThreadSafe {static final int THREAD_NUMBER = 2;static final int LOOP_NUMBER = 200;public static void main(String[] args) {// list 是局部变量,每个线程调用时会创建其不同实例,没有共享// 而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象// method3 的参数分析与 method2 相同MTest4ThreadUnsafe test = new MTest4ThreadUnsafe();for (int i = 0; i < THREAD_NUMBER; i++) {new Thread(() -> {test.method1(LOOP_NUMBER);}, "Thread" + i).start();}}public void method1(int loopNumber){ArrayList<String> list = new ArrayList<>();for (int i = 0; i < loopNumber; i++) {// 临界区,会产生竞态条件method2(list);method3(list);}}private void method2(ArrayList<String> list) {list.add("1");}private void method3(ArrayList<String> list) {list.remove(0);}
}class ThreadSafeSubClass extends MTest5ThreadSafe{public void method3(ArrayList<String> list) {new Thread(() -> {list.remove(0);}).start();}
}

常见线程安全类
String
Integer
StringBuffer
Random
Vector
Hashtable
java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。

它们的每个方法是原子的,但注意它们多个方法的组合不是原子的 。

// 如下,多个方法的组合不是原子Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {table.put("key", value);
}

在这里插入图片描述

4.2 案例详解

package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;import java.util.Random;@Slf4j(topic = "c.MTest7ExerciseTransfer")
public class MTest7ExerciseTransfer {public static void main(String[] args) throws InterruptedException {Account a = new Account(1000);Account b = new Account(1000);Thread t1 = new Thread(() -> {for (int i = 0; i < 1000; i++) {a.transfer(b, randomAmount());}}, "t1");Thread t2 = new Thread(() -> {for (int i = 0; i < 1000; i++) {b.transfer(a, randomAmount());}}, "t2");t1.start();t2.start();t1.join();t2.join();// 查看转账2000次后的总金额log.debug("total:{}",(a.getMoney() + b.getMoney()));}// Random 为线程安全static Random random = new Random();// 随机 1~100public static int randomAmount() {return random.nextInt(100) +1;}
}class Account {private int money;public Account(int money) {this.money = money;}public int getMoney() {return money;}public void setMoney(int money) {this.money = money;}public  void transfer(Account target, int amount) {if (this.money > amount) {this.setMoney(this.getMoney() - amount);target.setMoney(target.getMoney() + amount);}}
}// 运行结果
10:08:56.480 c.MTest7ExerciseTransfer [main] - total:4267//使用synchronized进行改进class Account {private int money;public Account(int money) {this.money = money;}public int getMoney() {return money;}public void setMoney(int money) {this.money = money;}public  void transfer(Account target, int amount) {synchronized (Account.class){ // 锁是Account类if (this.money > amount) {this.setMoney(this.getMoney() - amount);target.setMoney(target.getMoney() + amount);}}}
}
// 运行结果
10:10:38.624 c.MTest7ExerciseTransfer [main] - total:2000

5、Monitor

5.1 对象头

# 普通对象
|--------------------------------------------------------------|
|                    Object Header (64 bits)                   |
|------------------------------------|-------------------------|
|         Mark Word (32 bits)        |    Klass Word (32 bits) |
|------------------------------------|-------------------------|# 其中 Mark Word 结构为(32位虚拟机)
|-------------------------------------------------------|--------------------|
|                      Mark Word (32 bits)              |       State        |
|-------------------------------------------------------|--------------------|
|     hashcode:25     | age:4 | biased_lock:0 | 01      |       Normal       |  # 正常状态
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01      |       Biased       |  # 偏向锁
|-------------------------------------------------------|--------------------| 
|              ptr_to_lock_record:30          | 00      | Lightweight Locked |  # 轻量级锁
|-------------------------------------------------------|--------------------| 
|           ptr_to_heavyweight_monitor:30     | 10      | Heavyweight Locked |  # 重锁
|-------------------------------------------------------|--------------------|
|                                             | 11      |   Marked for GC    |
|-------------------------------------------------------|--------------------|# 64位虚拟机
|--------------------------------------------------------------------|--------------------|
|                     Mark Word (64 bits)                            |      State         |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01    |      Normal        |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2     | unused:1 | age:4 | biased_lock:1 | 01    |      Biased        |
|--------------------------------------------------------------------|--------------------|
|                      ptr_to_lock_record:62                 | 00    | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
|                  ptr_to_heavyweight_monitor:62             | 10    | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
|                                                             | 11   |    Marked for GC   |
|--------------------------------------------------------------------|--------------------|

5.2 Monitor(锁)

​ 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针 。

在这里插入图片描述

  • 刚开始 Monitor 中 Owner 为 null

  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner

  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入EntryList BLOCKED

  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的

  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程

5.3 Synchronized原理

static final Object lock = new Object();
static int counter = 0;public static void main(String[] args) {synchronized (lock) {counter++;}
}
Code:stack=2, locals=3, args_size=10: getstatic #2     // <- lock引用 (synchronized开始)3: dup4: astore_1         // lock引用 -> slot 15: monitorenter     // 将 lock对象 MarkWord 置为 Monitor 指针6: getstatic #3     // <- i9: iconst_1         // 准备常数 110: iadd            // +111: putstatic #3    // -> i14: aload_1         // <- lock引用15: monitorexit     // 将 lock对象 MarkWord 重置, 唤醒 EntryList16: goto 2419: astore_2        // e -> slot 220: aload_1         // <- lock引用21: monitorexit     // 将 lock对象 MarkWord 重置, 唤醒 EntryList22: aload_2         // <- slot 2 (e)23: athrow          // throw e24: returnException table:from to target type6  16 19 any19 22 19 any

5.4 Synchronized进阶

5.4.1 轻量级锁

轻量级锁的使用场景:如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。
轻量级锁对使用者是透明的,即语法仍然是 synchronized 。

static final Object obj = new Object();public static void method1() {synchronized( obj ) {// 同步块 Amethod2();}
}
public static void method2() {synchronized( obj ) {// 同步块 B}
}

在这里插入图片描述

1、创建锁记录(Lock Record)对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

2、让锁记录中 Object reference 指向锁对象,并尝试用 cas 替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录

​ 如果 cas 替换成功,对象头中存储了 锁记录地址和状态 00 ,表示由该线程给对象加锁 (如下图)

在这里插入图片描述

​ 如果 cas 失败,有两种情况
​ 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程

​ 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数 (如下图)

在这里插入图片描述

3、当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录,表示重入计数减一

4、 当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
成功,则解锁成功
失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程 。

5.4.2 锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

1、当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

在这里插入图片描述

2、这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址然后自己进入 Monitor 的 EntryList BLOCKED

在这里插入图片描述

3、当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程 。

5.4.3 自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

注意:

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋。

  • Java 7 之后不能控制是否开启自旋功能 。

5.4.4 偏向锁

static final Object obj = new Object();public static void m1() {synchronized( obj ) {// 同步块 Am2();}
}public static void m2() {synchronized( obj ) {// 同步块 Bm3();}
}public static void m3() {synchronized( obj ) {// 同步块 C}
}

在这里插入图片描述

如上图,轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。

Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS

在这里插入图片描述

一个对象创建时:如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
5.4.1 撤销偏向锁

1、调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销
轻量级锁会在锁记录中记录 hashCode
重量级锁会在 Monitor 中记录 hashCode

2、当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁 。

3、调用 wait/notify

5.4.2 批量重偏向

如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID
当撤销偏向锁阈值超过 20 次后,jvm 会这样觉得,我是不是偏向错了呢,于是会在给这些对象加锁时重新偏向至加锁线程

5.4.3 批量撤销

当撤销偏向锁阈值超过 40 次后,jvm 会这样觉得,自己确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的 。

5.4.5 锁消除

@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations=3)
@Measurement(iterations=5)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {static int x = 0;@Benchmarkpublic void a() throws Exception {x++;}@Benchmark // JIT即时编译器会进行锁消除优化public void b() throws Exception {Object o = new Object();synchronized (o) {x++;}}
}

6、Wait/Notify

6.1 Wait/Notify的简单介绍

  • obj.wait() 让进入 object 监视器的线程到 waitSet 等待

  • obj.notify() 在 object 上正在 waitSet 等待的线程中挑一个唤醒

  • obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒

注意:它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法 。

package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;
import static java.lang.Thread.sleep;@Slf4j(topic = "c.MTest8")
public class MTest8 {final static Object obj = new Object();public static void main(String[] args) throws InterruptedException {new Thread(() -> {synchronized (obj) {log.debug("执行....");try {obj.wait(); // 让线程在obj上一直等待下去} catch (InterruptedException e) {e.printStackTrace();}log.debug("其它代码....");}},"t1").start();new Thread(() -> {synchronized (obj) {log.debug("执行....");try {obj.wait(); // 让线程在obj上一直等待下去} catch (InterruptedException e) {e.printStackTrace();}log.debug("其它代码....");}},"t2").start();// 主线程两秒后执行sleep(2000);log.debug("唤醒 obj 上其它线程");synchronized (obj) {obj.notify(); // 唤醒obj上一个线程// obj.notifyAll(); // 唤醒obj上所有等待线程}}
}
# notify 的一种结果
14:27:24.172 c.MTest8 [t1] - 执行....
14:27:24.188 c.MTest8 [t2] - 执行....
14:27:26.182 c.MTest8 [main] - 唤醒 obj 上其它线程
14:27:26.182 c.MTest8 [t1] - 其它代码....# notifyAll 的结果
14:25:12.212 c.MTest8 [t1] - 执行....
14:25:12.212 c.MTest8 [t2] - 执行....
14:25:14.214 c.MTest8 [main] - 唤醒 obj 上其它线程
14:25:14.214 c.MTest8 [t2] - 其它代码....
14:25:14.214 c.MTest8 [t1] - 其它代码....

wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到notify 为止
wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify

sleep(long n) 和 wait(long n) 的区别

  1. sleep 是 Thread 方法,而 wait 是 Object 的方法

  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用

  3. sleep 在睡眠的同时,不会释放对象锁的,但 wait 在等待的时候会释放对象锁

  4. 它们状态 TIMED_WAITING

6.2 如何正确使用Wait/Notify

6.2.1 原始代码

package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;import static java.lang.Thread.sleep;@Slf4j(topic = "c.MTest9")
public class MTest9 {static final  Object room = new Object();static boolean hasCigarette = false;static boolean hasTakeout = false;public static void main(String[] args) throws InterruptedException {new Thread(() -> {synchronized (room){log.debug("有烟没?【{}】",hasCigarette);if(!hasCigarette){log.debug("没有烟,先休息一会!");try {sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有烟没?【{}】",hasCigarette);if(hasCigarette){log.debug("可以干活了!");}}},"tom").start();for (int i = 0; i < 5; i++) {new Thread(() -> {synchronized (room) {log.debug("可以开始干活了");}}, "其它人").start();}sleep(1000);new Thread(() -> {// 这里能不能加 synchronized (room)?hasCigarette = true;log.debug("烟到了噢!");}, "送烟的").start();}
}
14:48:48.419 c.MTest9 [tom] - 有烟没?【false】
14:48:48.419 c.MTest9 [tom] - 没有烟,先休息一会!
14:48:49.422 c.MTest9 [送烟的] - 烟到了噢!
14:48:50.421 c.MTest9 [tom] - 有烟没?【true】
14:48:50.421 c.MTest9 [tom] - 可以干活了!
14:48:50.421 c.MTest9 [其它人] - 可以开始干活了
14:48:50.421 c.MTest9 [其它人] - 可以开始干活了
14:48:50.421 c.MTest9 [其它人] - 可以开始干活了
14:48:50.421 c.MTest9 [其它人] - 可以开始干活了
14:48:50.421 c.MTest9 [其它人] - 可以开始干活了# 其它干活的线程,都要一直阻塞,效率太低
# tom线程必须睡足 2s 后才能醒来,就算烟提前送到,也无法立刻醒来
# 加了 synchronized (room) 后,就好比tom在里面反锁了门睡觉,烟根本没法送进门,main 没加synchronized 就好像 main 线程是翻窗户进来的# 下面使用 wait - notify 机制进行改进

6.2.2 改进代码-版本2

package com.yyds.juc.monitor;import static java.lang.Thread.sleep;import lombok.extern.slf4j.Slf4j;@Slf4j(topic = "c.MTest9V2")
public class MTest9V2 {static final  Object room = new Object();static boolean hasCigarette = false;static boolean hasTakeout = false;public static void main(String[] args) throws InterruptedException {new Thread(() -> {synchronized (room){log.debug("有烟没?【{}】",hasCigarette);if(!hasCigarette){log.debug("没有烟,先休息一会!");try {room.wait(2000);} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有烟没?【{}】",hasCigarette);if(hasCigarette){log.debug("可以干活了!");}}},"tom").start();for (int i = 0; i < 5; i++) {new Thread(() -> {synchronized (room) {log.debug("可以开始干活了");}}, "其它人").start();}sleep(1000);new Thread(() -> {synchronized (room) { hasCigarette = true;log.debug("烟到了噢!");// 唤醒room.notify();}}, "送烟的").start();}
}
14:58:59.780 c.MTest9V2 [tom] - 有烟没?【false】
14:58:59.780 c.MTest9V2 [tom] - 没有烟,先休息一会!
14:58:59.780 c.MTest9V2 [其它人] - 可以开始干活了
14:58:59.780 c.MTest9V2 [其它人] - 可以开始干活了
14:58:59.780 c.MTest9V2 [其它人] - 可以开始干活了
14:58:59.780 c.MTest9V2 [其它人] - 可以开始干活了
14:58:59.795 c.MTest9V2 [其它人] - 可以开始干活了
14:59:00.782 c.MTest9V2 [送烟的] - 烟到了噢!
14:59:00.782 c.MTest9V2 [tom] - 有烟没?【true】
14:59:00.782 c.MTest9V2 [tom] - 可以干活了!# 解决了其它干活的线程阻塞的问题
# 但如果有其它线程也在等待条件呢?

6.2.3 改进代码-版本3

package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;import static java.lang.Thread.sleep;@Slf4j(topic = "c.MTest9V3")
public class MTest9V3 {static final  Object room = new Object();static boolean hasCigarette = false;static boolean hasTakeout = false;public static void main(String[] args) throws InterruptedException {new Thread(() -> {synchronized (room){log.debug("有烟没?【{}】",hasCigarette);if(!hasCigarette){log.debug("没有烟,先休息一会!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有烟没?【{}】",hasCigarette);if(hasCigarette){log.debug("可以干活了!");}else {log.debug("没干成活...");}}},"tom").start();new Thread(() -> {synchronized (room){log.debug("有外卖没?【{}】",hasTakeout);if(!hasTakeout){log.debug("没有外卖,先休息一会!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有外卖没?【{}】",hasTakeout);if(hasTakeout){log.debug("可以干活了!");}else {log.debug("没干成活...");}}},"jerry").start();for (int i = 0; i < 5; i++) {new Thread(() -> {synchronized (room) {log.debug("可以开始干活了");}}, "其它人").start();}sleep(1000);new Thread(() -> {synchronized (room) {hasTakeout = true;log.debug("外卖到了噢!");// 唤醒(随机唤醒一个)room.notify();}}, "送外卖的").start();}
}
15:06:23.726 c.MTest9V3 [tom] - 有烟没?【false】
15:06:23.726 c.MTest9V3 [tom] - 没有烟,先休息一会!
15:06:23.726 c.MTest9V3 [其它人] - 可以开始干活了
15:06:23.726 c.MTest9V3 [其它人] - 可以开始干活了
15:06:23.726 c.MTest9V3 [其它人] - 可以开始干活了
15:06:23.726 c.MTest9V3 [其它人] - 可以开始干活了
15:06:23.726 c.MTest9V3 [其它人] - 可以开始干活了
15:06:23.726 c.MTest9V3 [jerry] - 有外卖没?【false】
15:06:23.726 c.MTest9V3 [jerry] - 没有外卖,先休息一会!
15:06:24.741 c.MTest9V3 [送外卖的] - 外卖到了噢!
15:06:24.741 c.MTest9V3 [tom] - 有烟没?【false】
15:06:24.741 c.MTest9V3 [tom] - 没干成活...# notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】
# 解决方法,改为 notifyAll

6.2.3 改进代码-版本4

package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;import static java.lang.Thread.sleep;@Slf4j(topic = "c.MTest9V4")
public class MTest9V4 {static final  Object room = new Object();static boolean hasCigarette = false;static boolean hasTakeout = false;public static void main(String[] args) throws InterruptedException {new Thread(() -> {synchronized (room){log.debug("有烟没?【{}】",hasCigarette);if(!hasCigarette){log.debug("没有烟,先休息一会!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有烟没?【{}】",hasCigarette);if(hasCigarette){log.debug("可以干活了!");}else {log.debug("没干成活...");}}},"tom").start();new Thread(() -> {synchronized (room){log.debug("有外卖没?【{}】",hasTakeout);if(!hasTakeout){log.debug("没有外卖,先休息一会!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有外卖没?【{}】",hasTakeout);if(hasTakeout){log.debug("可以干活了!");}else {log.debug("没干成活...");}}},"jerry").start();for (int i = 0; i < 5; i++) {new Thread(() -> {synchronized (room) {log.debug("可以开始干活了");}}, "其它人").start();}sleep(1000);new Thread(() -> {synchronized (room) {hasTakeout = true;log.debug("外卖到了噢!");// 全部唤醒room.notifyAll();}}, "送外卖的").start();}
}
15:09:32.757 c.MTest9V4 [tom] - 有烟没?【false】
15:09:32.757 c.MTest9V4 [tom] - 没有烟,先休息一会!
15:09:32.757 c.MTest9V4 [其它人] - 可以开始干活了
15:09:32.757 c.MTest9V4 [其它人] - 可以开始干活了
15:09:32.757 c.MTest9V4 [其它人] - 可以开始干活了
15:09:32.757 c.MTest9V4 [其它人] - 可以开始干活了
15:09:32.757 c.MTest9V4 [其它人] - 可以开始干活了
15:09:32.757 c.MTest9V4 [jerry] - 有外卖没?【false】
15:09:32.757 c.MTest9V4 [jerry] - 没有外卖,先休息一会!
15:09:33.759 c.MTest9V4 [送外卖的] - 外卖到了噢!
15:09:33.759 c.MTest9V4 [jerry] - 有外卖没?【true】
15:09:33.759 c.MTest9V4 [jerry] - 可以干活了!
15:09:33.759 c.MTest9V4 [tom] - 有烟没?【false】
15:09:33.759 c.MTest9V4 [tom] - 没干成活...# 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了
# 解决方法,用 while + wait,当条件不成立,再次 wait

6.2.3 改进代码-版本5

package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;import static java.lang.Thread.sleep;@Slf4j(topic = "c.MTest9V5")
public class MTest9V5 {static final  Object room = new Object();static boolean hasCigarette = false;static boolean hasTakeout = false;public static void main(String[] args) throws InterruptedException {new Thread(() -> {synchronized (room){log.debug("有烟没?【{}】",hasCigarette);while (!hasCigarette){log.debug("没有烟,先休息一会!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有烟没?【{}】",hasCigarette);if(hasCigarette){log.debug("可以干活了!");}else {log.debug("没干成活...");}}},"tom").start();new Thread(() -> {synchronized (room){log.debug("有外卖没?【{}】",hasTakeout);while(!hasTakeout){log.debug("没有外卖,先休息一会!");try {room.wait();} catch (InterruptedException e) {e.printStackTrace();}}log.debug("有外卖没?【{}】",hasTakeout);if(hasTakeout){log.debug("可以干活了!");}else {log.debug("没干成活...");}}},"jerry").start();for (int i = 0; i < 5; i++) {new Thread(() -> {synchronized (room) {log.debug("可以开始干活了");}}, "其它人").start();}sleep(1000);new Thread(() -> {synchronized (room) {hasTakeout = true;log.debug("外卖到了噢!");// 全部唤醒room.notifyAll();}}, "送外卖的").start();}
}
15:13:19.224 c.MTest9V5 [tom] - 有烟没?【false】
15:13:19.224 c.MTest9V5 [tom] - 没有烟,先休息一会!
15:13:19.224 c.MTest9V5 [其它人] - 可以开始干活了
15:13:19.224 c.MTest9V5 [其它人] - 可以开始干活了
15:13:19.224 c.MTest9V5 [其它人] - 可以开始干活了
15:13:19.224 c.MTest9V5 [其它人] - 可以开始干活了
15:13:19.224 c.MTest9V5 [其它人] - 可以开始干活了
15:13:19.224 c.MTest9V5 [jerry] - 有外卖没?【false】
15:13:19.224 c.MTest9V5 [jerry] - 没有外卖,先休息一会!
15:13:20.224 c.MTest9V5 [送外卖的] - 外卖到了噢!
15:13:20.224 c.MTest9V5 [jerry] - 有外卖没?【true】
15:13:20.224 c.MTest9V5 [jerry] - 可以干活了!
15:13:20.224 c.MTest9V5 [tom] - 没有烟,先休息一会!

总结一下:模板如下

synchronized(lock) {while(条件不成立) {lock.wait();}// 干活
}//另一个线程
synchronized(lock) {lock.notifyAll();
}

6.3 保护性暂停

6.3.1 基本概念

保护性暂停,即 Guarded Suspension,用在一个线程等待另一个线程的执行结果
要点

  • 有一个结果需要从一个线程传递到另一个线程,让他们关联同一个 GuardedObject

  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)

  • JDK 中,join 的实现、Future 的实现,采用的就是此模式

  • 因为要等待另一方的结果,因此归类到同步模式

在这里插入图片描述

6.3.2 简单实现

package com.yyds.juc.monitor;public class GuardedObject {private Object response;private final Object lock = new Object();/*** 获取response*/public Object get(){synchronized (lock){// 条件不满足,就等待while (response == null){try {lock.wait();} catch (InterruptedException e) {e.printStackTrace();}}return response;}}/*** 将数据获取给response*/public void complete(Object response){synchronized (lock){// 条件满足,通知等待线程this.response = response;lock.notifyAll();}}
}
package com.yyds.juc.monitor;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;public class Downloader {public static List<String> download() throws IOException {HttpURLConnection conn = (HttpURLConnection) new URL("https://www.baidu.com/").openConnection();List<String> lines = new ArrayList<>();try (BufferedReader reader =new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {String line;while ((line = reader.readLine()) != null) {lines.add(line);}}return lines;}
}
package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.util.List;@Slf4j(topic = "c.MTest0")
public class MTest0 {public static void main(String[] args) {GuardedObject guardedObject = new GuardedObject();// t2线程执行下载new Thread(() -> {List<String> response = null;try {response = Downloader.download();log.debug("download complete......");guardedObject.complete(response);} catch (IOException e) {e.printStackTrace();}},"t2").start();// t1线程获取t2线程的结果new Thread(() -> {log.debug("waiting...");Object response = guardedObject.get();log.debug("get response: [{}] lines", ((List<String>) response).size());},"t1").start();}
}
18:10:24.053 c.MTest0 [t1] - waiting...
18:10:24.977 c.MTest0 [t2] - download complete......
18:10:24.977 c.MTest0 [t1] - get response: [3] lines

6.3.3 带超时版实现

package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;@Slf4j(topic = "c.GuardedObject2")
public class GuardedObject2 {private Object response;private final Object lock = new Object();/*** 获取response*/public Object get(long millis){synchronized (lock){// 1、记录最初的时间long begin = System.currentTimeMillis();// 2、已经经历的时间long timePassed = 0;// 条件不满足,就等待while (response == null){// 3、还需要等待的时间long waitTime = millis - timePassed;log.debug("waitTime: {}", waitTime);if(waitTime <= 0){log.debug("break...");break;}try {lock.wait(waitTime);} catch (InterruptedException e) {e.printStackTrace();}// 4、如果提前被唤醒timePassed = System.currentTimeMillis() - begin;log.debug("timePassed: {}, object is null 【{}】", timePassed, response == null);}return response;}}/*** 将数据获取给response*/public void complete(Object response){synchronized (lock){// 条件满足,通知等待线程this.response = response;log.debug("notify...");lock.notifyAll();}}
}

测试

package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;import java.util.Arrays;
import java.util.List;import static java.lang.Thread.sleep;@Slf4j(topic = "c.MTest0Time")
public class MTest0Time {public static void main(String[] args) {GuardedObject2 guardedObject2 = new GuardedObject2();// t2线程结果传输给t1new Thread(() -> {try {sleep(1000);guardedObject2.complete(null);sleep(1000);guardedObject2.complete(Arrays.asList("hello","world"));} catch (InterruptedException e) {e.printStackTrace();}},"t2").start();Object response = guardedObject2.get(2500);if(response != null){log.debug("get response: [{}] lines", ((List<String>) response).size());}else {log.debug("can't get response");}}
}// 测试结果如下
10:31:22.105 c.GuardedObject2 [main] - waitTime: 2500
10:31:23.119 c.GuardedObject2 [t2] - notify...
10:31:23.119 c.GuardedObject2 [main] - timePassed: 1014, object is nulltrue10:31:23.119 c.GuardedObject2 [main] - waitTime: 1486
10:31:24.121 c.GuardedObject2 [t2] - notify...
10:31:24.121 c.GuardedObject2 [main] - timePassed: 2016, object is nullfalse10:31:24.121 c.MTest0Time [main] - get response: [2] lines

6.3.4 join原理

join体现就是保护性暂停。

t1.join();// 源码如下
public final void join() throws InterruptedException {join(0);
}public final synchronized void join(long millis) throws InterruptedException {long base = System.currentTimeMillis();long now = 0;if (millis < 0) {throw new IllegalArgumentException("timeout value is negative");}if (millis == 0) {// 调用者线程进入 t1 的 waitSet 等待, 直到 t1 运行结束while (isAlive()) {wait(0);}} else {while (isAlive()) {long delay = millis - now;if (delay <= 0) {break;}wait(delay);now = System.currentTimeMillis() - base;}}
}

6.3.5 多任务GuardedObject

​ 图中 Futures 就好比居民楼一层的信箱(每个信箱有房间编号),左侧的 t0,t2,t4 就好比等待邮件的居民,右侧的 t1,t3,t5 就好比邮递员。
​ 如果需要在多个类之间使用 GuardedObject 对象,作为参数传递不是很方便,因此设计一个用来解耦的中间类,这样不仅能够解耦【结果等待者】和【结果生产者】,还能够同时支持多个任务的管理 。

在这里插入图片描述

package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;@Slf4j(topic = "c.GuardedObject3")
public class GuardedObject3 {// 新增 id 用来标识 Guarded Objectprivate int id;public GuardedObject3(int id) {this.id = id;}public int getId() {return id;}private Object response;/*** 获取response*/public Object get(long millis){synchronized (this){// 1、记录最初的时间long begin = System.currentTimeMillis();// 2、已经经历的时间long timePassed = 0;// 条件不满足,就等待while (response == null){// 3、还需要等待的时间long waitTime = millis - timePassed;log.debug("waitTime: {}", waitTime);if(waitTime <= 0){log.debug("break...");break;}try {this.wait(waitTime);} catch (InterruptedException e) {e.printStackTrace();}// 4、如果提前被唤醒timePassed = System.currentTimeMillis() - begin;log.debug("timePassed: {}, object is null 【{}】", timePassed, response == null);}return response;}}/*** 将数据获取给response*/public void complete(Object response){synchronized (this){// 条件满足,通知等待线程this.response = response;log.debug("notify...");this.notifyAll();}}
}
package com.yyds.juc.monitor;import java.util.Hashtable;
import java.util.Map;
import java.util.Set;/*** 中间解耦的类*/
public class MailBoxes {private static Map<Integer, GuardedObject3> boxes = new Hashtable<>();private static int id = 1;// 产生唯一 idprivate static synchronized int generateId() {return id++;}public static GuardedObject3 getGuardedObject(int id) {return boxes.remove(id);}public static GuardedObject3 createGuardedObject() {GuardedObject3 go = new GuardedObject3(generateId());boxes.put(go.getId(), go);return go;}public static Set<Integer> getIds() {return boxes.keySet();}
}
package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;@Slf4j(topic = "c.MPeople")
public class MPeople extends Thread {@Overridepublic void run() {// 收信GuardedObject3 guardedObject = MailBoxes.createGuardedObject();log.debug("开始收信 id:{}", guardedObject.getId());Object mail = guardedObject.get(5000);log.debug("收到信 id:{}, 内容:{}", guardedObject.getId(), mail);}}package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;@Slf4j(topic = "c.MPostman")
public class MPostman extends Thread {private int id;private String mail;public MPostman(int id, String mail) {this.id = id;this.mail = mail;}@Overridepublic void run() {GuardedObject3 guardedObject = MailBoxes.getGuardedObject(id);log.debug("送信 id:{}, 内容:{}", id, mail);guardedObject.complete(mail);}
}

测试类

package com.yyds.juc.monitor;import static java.lang.Thread.sleep;public class MuliGuadeTest {public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 3; i++) {new MPeople().start();}sleep(1000);for (Integer id : MailBoxes.getIds()) {new MPostman(id, "内容" + id).start();}}
}// 测试结果如下
11:20:37.183 c.MPeople [Thread-0] - 开始收信 id:1
11:20:37.183 c.MPeople [Thread-2] - 开始收信 id:2
11:20:37.183 c.MPeople [Thread-1] - 开始收信 id:3
11:20:37.186 c.GuardedObject3 [Thread-2] - waitTime: 5000
11:20:37.186 c.GuardedObject3 [Thread-0] - waitTime: 5000
11:20:37.186 c.GuardedObject3 [Thread-1] - waitTime: 5000
11:20:38.195 c.MPostman [Thread-3] - 送信 id:3, 内容:内容3
11:20:38.195 c.MPostman [Thread-4] - 送信 id:2, 内容:内容2
11:20:38.195 c.GuardedObject3 [Thread-3] - notify...
11:20:38.195 c.GuardedObject3 [Thread-4] - notify...
11:20:38.195 c.MPostman [Thread-5] - 送信 id:1, 内容:内容1
11:20:38.195 c.GuardedObject3 [Thread-2] - timePassed: 1009, object is nullfalse11:20:38.195 c.GuardedObject3 [Thread-5] - notify...
11:20:38.195 c.GuardedObject3 [Thread-1] - timePassed: 1009, object is nullfalse11:20:38.195 c.MPeople [Thread-2] - 收到信 id:2, 内容:内容2
11:20:38.195 c.MPeople [Thread-1] - 收到信 id:3, 内容:内容3
11:20:38.195 c.GuardedObject3 [Thread-0] - timePassed: 1009, object is nullfalse11:20:38.195 c.MPeople [Thread-0] - 收到信 id:1, 内容:内容1

6.4 生产者/消费者

  • 与前面的保护性暂停中的 GuardObject 不同,不需要产生结果和消费结果的线程一一对应

  • 消费队列可以用来平衡生产和消费的线程资源

  • 生产者仅负责产生结果数据,不关心数据该如何处理,而消费者专心处理结果数据

  • 消息队列是有容量限制的,满时不会再加入数据,空时不会再消耗数据

  • JDK 中各种阻塞队列,采用的就是这种模式

在这里插入图片描述

代码实现如下

package com.yyds.juc.produce;public class Message {private int id;private Object message;public Message(int id, Object message) {this.id = id;this.message = message;}public int getId() {return id;}public Object getMessage() {return message;}
}
package com.yyds.juc.produce;import lombok.extern.slf4j.Slf4j;import java.util.LinkedList;@Slf4j(topic = "c.MessageQueue")
public class MessageQueue {private LinkedList<Message> queue;private int capacity;public MessageQueue(int capacity) {this.capacity = capacity;this.queue = new LinkedList<>();}public Message take() {synchronized (queue) {while (queue.isEmpty()) {log.debug("没货了, wait");try {queue.wait();} catch (InterruptedException e) {e.printStackTrace();}}Message message = queue.removeFirst();queue.notifyAll();return message;}}public void put(Message message) {synchronized (queue) {while (queue.size() == capacity) {log.debug("库存已达上限, wait");try {queue.wait();} catch (InterruptedException e) {e.printStackTrace();}}queue.addLast(message);queue.notifyAll();}}}
package com.yyds.juc.produce;import com.yyds.juc.monitor.Downloader;
import lombok.extern.slf4j.Slf4j;import java.io.IOException;
import java.util.List;@Slf4j(topic = "c.ProConTest")
public class ProConTest {public static void main(String[] args) {// 创建容量为2的队列MessageQueue messageQueue = new MessageQueue(2);// 4 个生产者线程, 下载任务for (int i = 0; i < 4; i++) {int id = i;new Thread(() -> {try {log.debug("download...");List<String> response = Downloader.download();log.debug("try put message({})", id);messageQueue.put(new Message(id, response));} catch (IOException e) {e.printStackTrace();}}, "生产者" + i).start();}// 1 个消费者线程, 处理结果new Thread(() -> {while (true) {Message message = messageQueue.take();List<String> response = (List<String>) message.getMessage();log.debug("take message({}): [{}] lines", message.getId(), response.size());}}, "消费者").start();}
}
// 测试结果如下
11:39:46.911 c.ProConTest [生产者0] - download...
11:39:46.911 c.MessageQueue [消费者] - 没货了, wait
11:39:46.911 c.ProConTest [生产者3] - download...
11:39:46.911 c.ProConTest [生产者1] - download...
11:39:46.911 c.ProConTest [生产者2] - download...
11:39:47.853 c.ProConTest [生产者2] - try put message(2)
11:39:47.853 c.ProConTest [生产者1] - try put message(1)
11:39:47.853 c.ProConTest [生产者3] - try put message(3)
11:39:47.853 c.ProConTest [生产者0] - try put message(0)
11:39:47.855 c.MessageQueue [生产者0] - 库存已达上限, wait
11:39:47.855 c.MessageQueue [生产者3] - 库存已达上限, wait
11:39:47.855 c.ProConTest [消费者] - take message(2): [3] lines
11:39:47.855 c.MessageQueue [生产者0] - 库存已达上限, wait
11:39:47.855 c.ProConTest [消费者] - take message(1): [3] lines
11:39:47.856 c.ProConTest [消费者] - take message(3): [3] lines
11:39:47.856 c.ProConTest [消费者] - take message(0): [3] lines
11:39:47.856 c.MessageQueue [消费者] - 没货了, wait

7、pack、unpack

// 暂停当前线程
LockSupport.park();// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)

与 Object 的 wait & notify 相比

  • wait,notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必

  • park & unpark 是以线程为单位来【阻塞】和【唤醒】线程,而 notify 只能随机唤醒一个等待线程,notifyAll是唤醒所有等待线程,就不那么【精确】

  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

7.1 先pack后unpack

在这里插入图片描述

1. 当前线程调用 Unsafe.park() 方法
2. 检查 _counter ,本情况为 0,这时,获得 _mutex 互斥锁
3. 线程进入 _cond 条件变量阻塞
4. 设置 _counter = 0  

在这里插入图片描述

1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2. 唤醒 _cond 条件变量中的 Thread_0
3. Thread_0 恢复运行
4. 设置 _counter 为 0

7.2 先unpack后pack

在这里插入图片描述

1. 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1
2. 当前线程调用 Unsafe.park() 方法
3. 检查 _counter ,本情况为 1,这时线程无需阻塞,继续运行
4. 设置 _counter 为 0

8、线程的6种状态

在这里插入图片描述

情况 1 NEW --> RUNNABLE

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

情况 2 RUNNABLE <–> WAITING

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

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

  • 调用 obj.notify() , obj.notifyAll() , t.interrupt() 时

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

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

情况 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

9、死锁、活锁、饥饿

9.1 死锁

一个线程需要同时获取多把锁,这时就容易发生死锁 。如下代码,就产生了死锁。

package com.yyds.juc.lock;import lombok.extern.slf4j.Slf4j;
import static java.lang.Thread.sleep;@Slf4j(topic = "c.DeadLockTest")
public class DeadLockTest {public static void main(String[] args) {Object A = new Object();Object B = new Object();Thread t1 = new Thread(() -> {synchronized (A) {log.debug("lock A");try {sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized (B) {log.debug("lock B");log.debug("操作...");}}}, "t1");Thread t2 = new Thread(() -> {synchronized (B) {log.debug("lock B");try {sleep(500);} catch (InterruptedException e) {e.printStackTrace();}synchronized (A) {log.debug("lock A");log.debug("操作...");}}}, "t2");t1.start();t2.start();}
}// 测试结果如下
21:47:10.154 c.DeadLockTest [t1] - lock A
21:47:10.154 c.DeadLockTest [t2] - lock B
# 定位死锁
# 检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁D:\juc\src\main\java\com\yyds\juc\monitor>jps
88304 Launcher
91744 DeadLockTest
92320 Jps
91716 KotlinCompileDaemon
41752 RemoteMavenServer36
43036D:\juc\src\main\java\com\yyds\juc\monitor>jstack 91744
2023-03-08 21:50:08
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.102-b14 mixed mode):"DestroyJavaVM" #14 prio=5 os_prio=0 tid=0x0000000002853800 nid=0x16640 waiting on condition [0x0000000000000000]java.lang.Thread.State: RUNNABLE"t2" #13 prio=5 os_prio=0 tid=0x000000001fcf5800 nid=0x149ec waiting for monitor entry [0x00000000202cf000]java.lang.Thread.State: BLOCKED (on object monitor)              # t2线程阻塞at com.yyds.juc.lock.DeadLockTest.lambda$main$1(DeadLockTest.java:38)- waiting to lock <0x000000076c8b0030> (a java.lang.Object) # 等待锁0x000000076c8b0030- locked <0x000000076c8b0040> (a java.lang.Object)          # t2锁住了0x000000076c8b0040at com.yyds.juc.lock.DeadLockTest$$Lambda$2/706277948.run(Unknown Source)at java.lang.Thread.run(Thread.java:745)"t1" #12 prio=5 os_prio=0 tid=0x000000001fcf5000 nid=0x16174 waiting for monitor entry [0x00000000201cf000]java.lang.Thread.State: BLOCKED (on object monitor)               # t1线程阻塞at com.yyds.juc.lock.DeadLockTest.lambda$main$0(DeadLockTest.java:23)- waiting to lock <0x000000076c8b0040> (a java.lang.Object)  # 等待锁0x000000076c8b0040- locked <0x000000076c8b0030> (a java.lang.Object)           # t2锁住了0x000000076c8b0030at com.yyds.juc.lock.DeadLockTest$$Lambda$1/687241927.run(Unknown Source)at java.lang.Thread.run(Thread.java:745)......Found one Java-level deadlock:
=============================
"t2":waiting to lock monitor 0x000000001c4beae8 (object 0x000000076c8b0030, a java.lang.Object),which is held by "t1"
"t1":waiting to lock monitor 0x000000001c4c0f58 (object 0x000000076c8b0040, a java.lang.Object),which is held by "t2"Java stack information for the threads listed above:
===================================================
"t2":at com.yyds.juc.lock.DeadLockTest.lambda$main$1(DeadLockTest.java:38)- waiting to lock <0x000000076c8b0030> (a java.lang.Object)- locked <0x000000076c8b0040> (a java.lang.Object)at com.yyds.juc.lock.DeadLockTest$$Lambda$2/706277948.run(Unknown Source)at java.lang.Thread.run(Thread.java:745)
"t1":at com.yyds.juc.lock.DeadLockTest.lambda$main$0(DeadLockTest.java:23)- waiting to lock <0x000000076c8b0040> (a java.lang.Object)- locked <0x000000076c8b0030> (a java.lang.Object)at com.yyds.juc.lock.DeadLockTest$$Lambda$1/687241927.run(Unknown Source)at java.lang.Thread.run(Thread.java:745)Found 1 deadlock.

在这里插入图片描述

9.2 活锁

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

package com.yyds.juc.lock;import lombok.extern.slf4j.Slf4j;import static java.lang.Thread.sleep;@Slf4j(topic = "c.LiveLockTest")
public class LiveLockTest {static volatile int count = 10;static final Object lock = new Object();public static void main(String[] args) {new Thread(() -> {// 期望减到 0 退出循环while (count > 0) {try {sleep(200);} catch (InterruptedException e) {e.printStackTrace();}count--;log.debug("count: {}", count);}}, "t1").start();new Thread(() -> {// 期望超过 20 退出循环while (count < 20) {try {sleep(200);} catch (InterruptedException e) {e.printStackTrace();}count++;log.debug("count: {}", count);}}, "t2").start();}
}

9.3 饥饿

使用顺序加锁的方式,解决之前的死锁问题。

下面图展示了死锁问题

在这里插入图片描述

通过顺序加锁进行解决

在这里插入图片描述

饥饿是一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束 。顺序加锁可能会产生饥饿。

10、ReentrantLock

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

相对于 synchronized 它具备如下特点

  • 可中断

  • 可以设置超时时间

  • 可以设置为公平锁

  • 支持多个条件变量
    不过,与 synchronized 一样,都支持可重入

10.1 可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住

package com.yyds.juc.lock;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.ReentrantLock;@Slf4j(topic = "c.ReentrantLockTest")
public class ReentrantLockTest {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();}}
}
// 测试结果
22:25:22.766 c.ReentrantLockTest [main] - execute method1
22:25:22.767 c.ReentrantLockTest [main] - execute method2
22:25:22.767 c.ReentrantLockTest [main] - execute method3

10.2 可打断

package com.yyds.juc.lock;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.ReentrantLock;import static java.lang.Thread.sleep;@Slf4j(topic = "c.ReentrantLockInterrupt")
public class ReentrantLockInterrupt {public static void main(String[] args) {ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("启动...");try {// 注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断lock.lockInterruptibly();} catch (InterruptedException e) {e.printStackTrace();log.debug("等锁的过程中被打断");return;}},"t1");lock.lock();log.debug("获得了锁");t1.start();try {sleep(1000);t1.interrupt();log.debug("执行打断");} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}
}// 测试结果如下
11:17:34.554 c.ReentrantLockInterrupt [main] - 获得了锁
11:17:34.554 c.ReentrantLockInterrupt [t1] - 启动...
11:17:35.555 c.ReentrantLockInterrupt [main] - 执行打断
11:17:35.555 c.ReentrantLockInterrupt [t1] - 等锁的过程中被打断
java.lang.InterruptedExceptionat 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.yyds.juc.lock.ReentrantLockInterrupt.lambda$main$0(ReentrantLockInterrupt.java:20)at java.lang.Thread.run(Thread.java:745)

10.3 锁超时

package com.yyds.juc.lock;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;import static java.lang.Thread.sleep;@Slf4j(topic = "c.ReentrantLockOverTime")
public class ReentrantLockOverTime {public static void main(String[] args) throws InterruptedException {ReentrantLock lock = new ReentrantLock();Thread t1 = new Thread(() -> {log.debug("启动...");try {if(!lock.tryLock(2L, TimeUnit.SECONDS)){log.debug("获取锁超过2S,获取失败,返回");return;}} catch (InterruptedException e) {e.printStackTrace();}try {log.debug("获得了锁");} finally {lock.unlock();}},"t1");// 主线程先获取锁lock.lock();log.debug("获得了锁");t1.start();try {sleep(3000);} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}
}
// 测试结果如下
11:37:26.249 c.ReentrantLockOverTime [main] - 获得了锁
11:37:26.249 c.ReentrantLockOverTime [t1] - 启动...
11:37:28.263 c.ReentrantLockOverTime [t1] - 获取锁超过2S,获取失败,返回

10.3.1 哲学家就餐问题

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

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

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

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

筷子类

package com.yyds.juc.lock;/*** 筷子类*/
public class Chopstick {String name;public Chopstick(String name) {this.name = name;}@Overridepublic String toString() {return "筷子{" + name + '}';}
}

哲学家类

package com.yyds.juc.lock;import lombok.extern.slf4j.Slf4j;/*** 哲学家类*/
@Slf4j(topic = "c.Philosopher")
public 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() throws InterruptedException {log.debug("eating...");sleep(1000);}@Overridepublic void run()  {while (true) {// 获得左手筷子synchronized (left) {// 获得右手筷子synchronized (right) {// 吃饭try {eat();} catch (InterruptedException e) {e.printStackTrace();}}// 放下右手筷子}// 放下左手筷子}}
}

测试类

package com.yyds.juc.lock;public class PhilosopherTest {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();}
}
// 会发现执行后,出现死锁

10.3.2 利用 tryLock解决哲学家就餐问题

筷子类继承ReentrantLock

package com.yyds.juc.lock;import java.util.concurrent.locks.ReentrantLock;/*** 筷子类*/
public class ChopstickV2 extends ReentrantLock {String name;public ChopstickV2(String name) {this.name = name;}@Overridepublic String toString() {return "筷子{" + name + '}';}
}

哲学家类利用tryLock

package com.yyds.juc.lock;import lombok.extern.slf4j.Slf4j;/*** 哲学家类*/
@Slf4j(topic = "c.PhilosopherV2")
public class PhilosopherV2 extends Thread {ChopstickV2 left;ChopstickV2 right;public PhilosopherV2(String name, ChopstickV2 left, ChopstickV2 right) {super(name);this.left = left;this.right = right;}private void eat() throws InterruptedException {log.debug("eating...");sleep(1000);}@Overridepublic void run()  {while (true) {// 尝试获得左手筷子if(left.tryLock()){try {// 尝试获取右手筷子if(right.tryLock()){try {eat();} catch (InterruptedException e) {e.printStackTrace();} finally {right.unlock();}}} finally {// 右手筷子没有获取,就释放左手筷子left.unlock();}}}}
}

测试类

package com.yyds.juc.lock;public class PhilosopherTestV2 {public static void main(String[] args) {ChopstickV2 c1 = new ChopstickV2("1");ChopstickV2 c2 = new ChopstickV2("2");ChopstickV2 c3 = new ChopstickV2("3");ChopstickV2 c4 = new ChopstickV2("4");ChopstickV2 c5 = new ChopstickV2("5");new PhilosopherV2("苏格拉底", c1, c2).start();new PhilosopherV2("柏拉图", c2, c3).start();new PhilosopherV2("亚里士多德", c3, c4).start();new PhilosopherV2("赫拉克利特", c4, c5).start();new PhilosopherV2("阿基米德", c5, c1).start();}
}

10.4 条件变量

synchronized 中也有条件变量,就是那个 waitSet 休息室,当条件不满足时进入 waitSet 等待
ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比

  • synchronized 是那些不满足条件的线程都在一间休息室等消息

  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:

  • await 前需要获得锁

  • await 执行后,会释放锁,进入 conditionObject 等待

  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁

  • 竞争 lock 锁成功后,从 await 后继续执行

package com.yyds.juc.lock;import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;import static java.lang.Thread.sleep;@Slf4j(topic = "c.ReentracntLockCondition")
public class ReentracntLockCondition {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) throws InterruptedException {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(1000);sendBreakfast();sleep(1000);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();}}
}
// 测试结果如下
15:49:12.650 c.ReentracntLockCondition [main] - 送早餐来了
15:49:12.652 c.ReentracntLockCondition [早餐] - 等到了早餐
15:49:13.655 c.ReentracntLockCondition [main] - 送烟来了
15:49:13.655 c.ReentracntLockCondition [] - 等到了烟

10.5 线程之间的顺序控制

10.5.1 通过wait/notify固定顺序控制

先 2 后 1 打印

package com.yyds.juc.order;import lombok.extern.slf4j.Slf4j;@Slf4j(topic = "c.WaitNotifyOrder")
public class WaitNotifyOrder {static Object obj = new Object();// t2 运行标记, 代表 t2 是否执行过static boolean t2runed = false;public static void main(String[] args) {new Thread(() -> {synchronized (obj){// t2没有执行过,就等待while (!t2runed){try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}}log.debug("1");},"t1").start();new Thread(() -> {log.debug("2");synchronized (obj){// 修改运行标记t2runed = true;// 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)obj.notifyAll();}},"t2").start();}
}
// 测试结果如下
17:40:08.113 c.WaitNotifyOrder [t2] - 2
17:40:08.115 c.WaitNotifyOrder [t1] - 1

10.5.2 通过park/unpark固定顺序控制

先 2 后 1 打印

package com.yyds.juc.order;
import lombok.extern.slf4j.Slf4j;import java.util.concurrent.locks.LockSupport;@Slf4j(topic = "c.ParkUnparkOrder")
public class ParkUnparkOrder {public static void main(String[] args) {/*** 没有『许可』时,当前线程暂停运行;* 有『许可』时,用掉这个『许可』,当前线程恢复运行*/Thread t1 =  new Thread(() -> {// 暂停当前线程LockSupport.park();log.debug("t1");},"t1");/*** 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)*/Thread t2 =   new Thread(() -> {log.debug("t2");LockSupport.unpark(t1);},"t1");t1.start();t2.start();}
}
// 测试结果如下
17:33:37.742 c.ParkUnparkOrder [t1] - t2
17:33:37.742 c.ParkUnparkOrder [t1] - t1

10.5.3 通过wait/notify交替输出

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

package com.yyds.juc.order;public 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();}}}
}
package com.yyds.juc.order;import lombok.extern.slf4j.Slf4j;@Slf4j(topic = "c.WaitNotifyOneByOne")
public class WaitNotifyOneByOne {public static void main(String[] args) {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();}
}
// 测试结果如下
abcabcabcabcabc

10.5.4 通过ReentrantLock交替输出

package com.yyds.juc.order;import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class ReentrantOneByOne {public static void main(String[] args) throws InterruptedException {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();// 先唤醒a线程Thread.sleep(1000);as.lock();try {aWaitSet.signal();}finally {as.unlock();}}
}
// 测试结果如下
abcabcabcabcabcclass AwaitSignal extends ReentrantLock{// 循环次数private int loopNumber;public AwaitSignal(int loopNumber) {this.loopNumber = loopNumber;}/**** @param str 要打印的内容* @param current 当前休息室* @param next 下一个休息室*/public void print(String str, Condition current, Condition next) {for (int i = 0; i < loopNumber; i++) {this.lock();try {current.await();System.out.print(str);next.signal();} catch (InterruptedException e) {e.printStackTrace();} finally {this.unlock();}}}}

10.5.5 通过park/unpark交替输出

package com.yyds.juc.order;import java.util.concurrent.locks.LockSupport;public 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]);}}class TestUnpark{public static void main(String[] args) {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();}
}// 测试结果如下
abc
abc
abc
abc
abc

相关文章:

java并发入门(一)共享模型—Synchronized、Wait/Notify、pack/unpack

一、共享模型—管程 1、共享存在的问题 1.1 共享变量案例 package com.yyds.juc.monitor;import lombok.extern.slf4j.Slf4j;Slf4j(topic "c.MTest1") public class MTest1 {static int counter 0;public static void main(String[] args) throws InterruptedEx…...

Ast2500增加用户自定义功能

备注&#xff1a;这里使用的AMI的开发环境MegaRAC进行AST2500软件开发&#xff0c;并非openlinux版本。1、添加上电后自动执行的任务在PDKAccess.c中列出了系统启动过程中的所有任务&#xff0c;若需要添加功能&#xff0c;在相应的任务中添加自定义线程。一般在两个任务里面添…...

用Python暴力求解德·梅齐里亚克的砝码问题

文章目录固定个数的砝码可称量重量砝码的组合方法40镑砝码的组合问 一个商人有一个40磅的砝码&#xff0c;由于跌落在地而碎成4块。后来&#xff0c;称得每块碎片的重量都是整磅数&#xff0c;而且可以用这4 块来称从1 至40 磅之间的任意整数磅的重物。问这4 块砝码片各重多少&…...

离散Hopfield神经网络的分类——高校科研能力评价

离散Hopfield网络离散Hopfield网络是一种经典的神经网络模型&#xff0c;它的基本原理是利用离散化的神经元和离散化的权值矩阵来实现模式识别和模式恢复的功能。它最初由美国物理学家John Hopfield在1982年提出&#xff0c;是一种单层的全连接神经网络&#xff0c;被广泛应用于…...

Retrofit核心源码分析(三)- Call逻辑分析和扩展机制

在前面的两篇文章中&#xff0c;我们已经对 Retrofit 的注解解析、动态代理、网络请求和响应处理机制有了一定的了解。在这篇文章中&#xff0c;我们将深入分析 Retrofit 的 Call 逻辑&#xff0c;并介绍 Retrofit 的扩展机制。 一、Call 逻辑分析 Call 是 Retrofit 中最基本…...

源码分析spring如和对@Component注解进行BeanDefinition注册的

Spring ioc主要职责为依赖进行处理&#xff08;依赖注入、依赖查找&#xff09;、容器以及托管的(java bean、资源配置、事件)资源声明周期管理&#xff1b;在ioc容器启动对元信息进行读取&#xff08;比如xml bean注解等&#xff09;、事件管理、国际化等处理&#xff1b;首先…...

C语言--字符串函数1

目录前言strlenstrlen的模拟实现strcpystrcatstrcat的模拟实现strcmpstrcmp的模拟实现strncpystrncatstrncmpstrstrstrchr和strrchrstrstr的模拟实现前言 本章我们将重点介绍处理字符和字符串的库函数的使用和注意事项。 strlen 我们先来看一个我们最熟悉的求字符串长度的库…...

Webstorm使用、nginx启动、FinalShell使用

文章目录 主题设置FinalShellFinalShell nginx 启动历史命令Nginx页面发布配置Webstorm的一些常用快捷键代码生成字体大小修改Webstorm - gitCode 代码拉取webstorm 汉化webstorm导致CPU占用率高方法一 【忽略node_modules】方法二 【设置 - 代码编辑 - 快速预览文档 - 关闭】主…...

源码分析Spring @Configuration注解如何巧夺天空,偷梁换柱。

前言 回想起五年前的一次面试&#xff0c;面试官问Configuration注解和Component注解有什么区别&#xff1f;记得当时的回答是&#xff1a; 相同点&#xff1a;Configuration注解继承于Component注解&#xff0c;都可以用来通过ClassPathBeanDefinitionScanner装载Spring bean…...

vector的使用及模拟实现

目录 一.vector的介绍及使用 1.vector的介绍 2.vector的使用 1.vector的定义 2.vector iterator的使用 3. vector 空间增长问题 4.vector 增删查改 3.vector 迭代器失效问题&#xff08;重点&#xff09; 1. 会引起其底层空间改变的操作 2.指定位置元素的删除操作--erase 3. Li…...

“华为杯”研究生数学建模竞赛2007年-【华为杯】A题:基于自助法和核密度估计的膳食暴露评估模型(附获奖论文)

赛题描述 我国是一个拥有13亿人口的发展中国家,每天都在消费大量的各种食品,这批食品是由成千上万的食品加工厂、不可计数的小作坊、几亿农民生产出来的,并且经过较多的中间环节和长途运输后才为广大群众所消费,加之近年来我国经济发展迅速而环境治理没有能够完全跟上,以…...

刷题(第三周)

目录 [CISCN2021 Quals]upload [羊城杯 2020]EasySer [网鼎杯 2020 青龙组]notes [SWPU2019]Web4 [Black Watch 入群题]Web [HFCTF2020]BabyUpload [CISCN2021 Quals]upload 打开界面以后&#xff0c;发现直接给出了源码 <?php if (!isset($_GET["ctf"]))…...

新C++(14):移动语义与右值引用

当你在学习语言的时候&#xff0c;是否经常听到过一种说法,""左边的叫做左值&#xff0c;""右边的叫做右值。这句话对吗&#xff1f;从某种意义上来说&#xff0c;这句话只是说对了一部分。---前言一、什么是左右值?通常认为:左值是一个表示数据的表达式(…...

TCP相关概念

目录 一.滑动窗口 1.1概念 1.2滑动窗口存在的意义 1.3 滑动窗口的大小变化 1.4丢包问题 二.拥塞控制 三.延迟应答 四.捎带应答 五.面向字节流 六.粘包问题 七.TIME_WAIT状态 八.listen第2个参数 九.TCP总结 一.滑动窗口 1.1概念 概念&#xff1a;双方在进行通信时&a…...

MySQL锁篇

MySQL锁篇 一、一条update语句 我们的故事继续发展&#xff0c;我们还是使用t这个表&#xff1a; CREATE TABLE t (id INT PRIMARY KEY,c VARCHAR(100) ) EngineInnoDB CHARSETutf8;现在表里的数据就是这样的&#xff1a; mysql> SELECT * FROM t; —------- | id | c | —…...

SWF (Simple Workflow Service)简介

Amazon Simple Workflow Service (Amazon SWF) 提供了给应用程序异步、分布式处理的流程工具。 SWF可以用在媒体处理、网站应用程序后端、商业流程、数据分析和一系列定义好的任务上。 举个例子&#xff0c;下图表明了一个电商网站的工作流程&#xff0c;其中涉及了程序执行的…...

java(Class 常用方法 获取Class对象六种方式 动态和静态加载 类加载流程)

ClassClass常用方法获取Class对象六种方式哪些类型有Class对象动态和静态加载类加载流程加载阶段连接阶段连接阶段-验证连接阶段-准备连接阶段-解析初始化阶段获取类结构信息Class常用方法 第一步&#xff1a;创建一个实体类 public class Car {public String brand "宝…...

【数据结构】线性表和顺序表

Yan-英杰的主页 悟已往之不谏 知来者之可追 目录 1.线性表 2.顺序表 2.1 静态顺序表 2.2 动态顺序表 2.3移除元素 1.线性表 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构&#xff0c;常见的线…...

Ubuntu数据库安装(mysql)

##1.下载mysql-apt-config_0.8.22-1_all.deb并且安装 wget https://dev.mysql.com/get/mysql-apt-config_0.8.22-1_all.deb sudo dpkg -i mysql-apt-config_0.8.22-1_all.deb##2.更新apt-updata sudo apt update##3.如果出现如下图情况执行以下命令 [外链图片转存失败,源站可…...

MyBatis-Plus的入门学习

MyBatis-Plus入门学习简介特性快速开始MyBatis-Plus的注解详解Tableld主键生成策略1、数据库自动增长 AUTO2、UUID3、Redis生成id4、MP主键自动生成TableNameTableField自动填充测试方法&#xff1a;update乐观锁select查所有根据id查多个id批量查询简单条件查询&#xff08;通…...

华为OD机试题 - 内存池(JavaScript)

更多题库,搜索引擎搜 梦想橡皮擦华为OD 👑👑👑 更多华为OD题库,搜 梦想橡皮擦 华为OD 👑👑👑 更多华为机考题库,搜 梦想橡皮擦华为OD 👑👑👑 华为OD机试题 最近更新的博客使用说明本篇题解:内存池题目输入输出示例一输入输出说明Code解题思路版权说明华为…...

数据库索引原理

数据库索引的作用是做数据的快速检索&#xff0c;而快速检索实现的本质是数据结构。像二叉树、红黑树、AVL树、B树、B树、哈希等数据结构都可以实现索引&#xff0c;但其中B树效率最高。MySQL数据库索引使用的是B树。二叉树&#xff1a;二叉树中&#xff0c;左子树比根节点小&a…...

字符函数和字符串函数详解(1)

目录前言strlen函数strlensizeofstrcpy函数strcat函数strcmp函数总结前言 最近要调整状态&#xff0c;写的文章质量不佳让大家失望&#xff0c;我现在也在反思我在做什么&#xff0c;我会什么&#xff0c;我学了什么。等我想明白的那天&#xff0c;我一定能跟大家顶峰相见的&a…...

【数据分析:工具篇】NumPy(1)NumPy介绍

【数据分析&#xff1a;工具篇】NumPy&#xff08;1&#xff09;NumPy介绍NumPy介绍NumPy的特点数组的基本操作创建数组索引和切片数组运算NumPy介绍 NumPy&#xff08;Numerical Python&#xff09;是Python的一个开源的科学计算库&#xff0c;它主要用于处理大规模的多维数组…...

mysql时区问题

设置mysql容器时间与服务器时间一致 问题背景&#xff1a; 今天测试发现一个问题&#xff0c;时间不一致&#xff0c;当工单入库时&#xff0c;其创建时间和更新时间应该是一样的&#xff0c;即使不一样最多只会错几秒的时间&#xff1b;实际上两个时间相差的大概8小时&#…...

磨金石教育摄影技能干货分享|高邮湖上观花海

江苏高邮&#xff0c;说到这里所有人能想到的&#xff0c;就是那烟波浩渺的高邮湖。高邮在旅游方面并不出名&#xff0c;但是这里的自然人文景观绝对不输于其他地方。高邮不止有浩瀚的湖泊&#xff0c;春天的油菜花海同样壮观。春日的午后&#xff0c;与家人相约游玩&#xff0…...

mysql navicat忘记密码

mysql忘记密码是常用的事情&#xff0c;那么如何解决它呢&#xff1f;1、首先将MySQL的服务关闭&#xff0c;两种方法&#xff1a;&#xff08;1&#xff09;打开命令行cmd输入net stop mysql命令即可关闭MySQL服务。&#xff08;2&#xff09;打开任务管理器&#xff0c;找到服…...

Git的下载、安装、配置、使用、卸载

前言 我是跟着狂神老师学的。该博客仅用于笔记所用。 下面是老师的B站和笔记 B站&#xff1a;https://www.bilibili.com/video/BV1FE411P7B3?p1&vd_source9266cf72b1f398b63abe0aefe358d7d6 笔记&#xff1a;https://mp.weixin.qq.com/s/Bf7uVhGiu47uOELjmC5uXQ 一、准备工…...

【博客631】监控网卡与进程网络IO使用情况

监控进程的网络IO使用情况 1、vnstat 由于 vnstat 依赖于内核提供的信息&#xff0c;因此执行以下命令来验证内核是否提供了 vnStat 所期望的所有信息&#xff1a; # vnstat --testkernel This test will take about 60 seconds. Everything is ok.不带任何参数的 vnstat 将…...

【Leetcode】【简单】35. 搜索插入位置

给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 示例 1: 输入: nums [1,3,5,6], target 5 输出: 2 示例 2: 输入:…...

sql面试题

mysql优化 优化准则&#xff1a; 建表时&#xff1a;合理选择字段的类型&#xff0c;单表字段数量 sql查询尽量单表操作&#xff0c;避免复杂操作&#xff0c;复杂的多表通过java代码实现 构建复合索引优化&#xff0c;索引尽量可以覆盖主要业务查询 sql避免索引失效 避免大…...

SQL 进阶刷题笔记

SQL 进阶刷题笔记 一、MySQL 进阶 这里主要是 MySQL 刷题相关笔记&#xff0c;方便后面温习和查阅&#xff0c;希望可以帮到大家&#xff01;&#xff01;&#xff01; 题1 请计算每张SQL类别试卷发布后&#xff0c;当天5级以上的用户作答的人数uv和平均分avg_score&#xff0…...

[网鼎杯 2020 朱雀组]Think Java

SqlDict.java ,其中sql语句处存在sql注入漏洞 package .sqldict;import cn.abc.core.sqldict.Row; import cn.abc.core.sqldict.Table; import java...

AIR32F103(十) 在无系统环境和FreeRTOS环境集成LVGL

目录 AIR32F103(一) 合宙AIR32F103CBT6开发板上手报告AIR32F103(二) Linux环境和LibOpenCM3项目模板AIR32F103(三) Linux环境基于标准外设库的项目模板AIR32F103(四) 27倍频216MHz,CoreMark跑分测试AIR32F103(五) FreeRTOSv202112核心库的集成和示例代码AIR32F103(六) ADC,I2S…...

SpringBoot接口 - 如何统一异常处理

SpringBoot接口如何对异常进行统一封装&#xff0c;并统一返回呢&#xff1f;以上文的参数校验为例&#xff0c;如何优雅的将参数校验的错误信息统一处理并封装返回呢&#xff1f;为什么要优雅的处理异常如果我们不统一的处理异常&#xff0c;经常会在controller层有大量的异常…...

如何使用Python进行数据可视化

数据可视化是一种将数据呈现为图形或图表的技术&#xff0c;它有助于理解和发现数据中的模式和趋势。Python是一种流行的编程语言&#xff0c;有很多库可以帮助我们进行数据可视化。在本文中&#xff0c;我们将介绍使用Python进行数据可视化的基本步骤。 第一步&#xff1a;导…...

vue -- 自定义指令钩子函数补充 自定义过滤器filter参数

自定义指令补充 自定义指令通过钩子函数的形式来实现自定义的功能 这里是几个常用的钩子函数以及它的方法&#xff1a; bind&#xff1a;只调用一次&#xff0c;指令第一次绑定到元素时调用&#xff0c;在这里可以进行一次性的初始化设置。 inserted&#xff1a;被绑定元素插…...

Qt不会操作?Qt原理不知道? | Qt详细讲解

文章目录Qt界面开发必备知识UI界面与控件类型介绍Qt设计器原理控件类型的介绍信号与槽机制处理常用控件创建与设置常见展示型控件创建与设置常见动作型控件创建与设置常见输入型控件创建与设置常见列表控件创建于设置Qt中对象树的介绍项目源码结构刨析.pro.hmain.cpp.cppQt界面…...

LeetCode-面试题 17.05. 字母与数字【前缀和,哈希表】

LeetCode-面试题 17.05. 字母与数字【前缀和&#xff0c;哈希表】题目描述&#xff1a;解题思路一&#xff1a;前缀和。数字为-1&#xff0c;字母为1。我们需要找到的子数组是前缀和之差为0的&#xff0c;例如s[right]-s[left]0&#xff0c;那么s[right]s[left]&#xff0c;变为…...

华为OD机试题 - 叠放书籍(JavaScript)| 机考必刷

更多题库,搜索引擎搜 梦想橡皮擦华为OD 👑👑👑 更多华为OD题库,搜 梦想橡皮擦 华为OD 👑👑👑 更多华为机考题库,搜 梦想橡皮擦华为OD 👑👑👑 华为OD机试题 最近更新的博客使用说明本篇题解:叠放书籍题目输入输出示例一输入输出Code解题思路版权说明华为O…...

【数据库概论】第十一章 数据库并发控制

第十一章 并发控制 在多处理机系统中&#xff0c;每个处理机可以运行一个事务&#xff0c;多个处理机可以同时运行多个事务&#xff0c;实现多个事务并行运行&#xff0c;这就是同时并发方式。当多个用户并发存取数据库时会产生多个事务同时存取同一事务的情况&#xff0c;如果…...

Nginx配置实例-反向代理案例二

实现效果&#xff1a;使用nginx反向代理&#xff0c;根据访问的路径跳转到不同端口服务 nginx监听端口为9000&#xff0c; 访问 http://127.0.0.1:9000/edu/ 直接跳转到127.0.0.1:8080 访问 http://127.0.0.1:9000/vod/ 直接跳转到127.0.0.1:8081 一、准备工作 1. 准备两个tom…...

HTML 字符集

为了正确显示 HTML 页面&#xff0c;Web 浏览器必须知道要使用哪个字符集。 从 ASCII 到 UTF-8 ASCII 是第一个字符编码标准。ASCII 定义了 128 种可以在互联网上使用的字符&#xff1a;数字&#xff08;0-9&#xff09;、英文字母&#xff08;A-Z&#xff09;和一些特殊字符…...

【C语言】每日刷题 —— 牛客语法篇(3)

前言 大家好&#xff0c;继续更新专栏c_牛客&#xff0c;不出意外的话每天更新十道题&#xff0c;难度也是从易到难&#xff0c;自己复习的同时也希望能帮助到大家&#xff0c;题目答案会根据我所学到的知识提供最优解。 &#x1f3e1;个人主页&#xff1a;悲伤的猪大肠9的博客…...

基于Vue3和element-plus实现一个完整的登录功能

先看一下最终要实现的效果:登录页面:注册页面:(1)引入element-plus组件库引入组件库的方式有好多种,在这里我就在main.js全局引入了.npm i element-plus -Smain.js中代码:import { createApp } from "vue"; //element-plus import ElementPlus from "element-pl…...

【java】Java 中泛型的实现原理

文章目录前序1. 泛型1.1 泛型方法1.2 泛型类1.3 泛型接口2. 泛型的基本原理3. 小结前序 泛型是 Java 开发中常用的技术&#xff0c;了解泛型的几种形式和实现泛型的基本原理&#xff0c;有助于写出更优质的代码。本文总结了 Java 泛型的三种形式以及泛型实现原理。 1. 泛型 …...

【C++提高编程】C++全栈体系(二十七)

C提高编程 第五章 STL- 常用算法 三、常用排序算法 算法简介&#xff1a; sort //对容器内元素进行排序random_shuffle //洗牌 指定范围内的元素随机调整次序merge // 容器元素合并&#xff0c;并存储到另一容器中reverse // 反转指定范围的元素 1. sort 功能描述&#…...

软考高级信息系统项目管理师系列之三十九:项目集管理

软考高级信息系统项目管理师系列之三十九:项目集管理 一、项目集管理内容二、项目集管理基础概述1.项目集定义2.项目集活动3.项目集管理三、项目集的管理过程四、项目集治理1.项目集治理概述2.项目集指导委员会的职责3.项目集治理功能五、项目集生命周期1.项目集生命周期三个阶…...

44-Golang中的channel

Golang中的channel为什么要使用channelchannel的介绍channel的基本使用定义/声明channel管道的遍历和关闭channel的关闭channel的遍历goroutine和channel结合应用实例1应用实例2案例注意事项为什么要使用channel 前面使用全局变量加锁同步来解决goroutine的通讯&#xff0c;但…...

80/20法则

80/20法则&#xff08;The 80/20 Rule&#xff09;又称为帕累托法则(Pareto Principle&#xff09;、二八定律、帕累托定律、最省力法则、不平衡原则、犹太法则、马特莱法则等一、什么是80/20法则80/20法则&#xff08;The 80/20 Rule&#xff09;&#xff0c;又称为帕累托法则…...