并发4️⃣管程⑥线程执行顺序控制、管程知识点小结
1、线程执行顺序控制
1.1、说明
执行顺序
- 固定顺序:如 t1 → t2 → t3。
- 交替执行:如 t1 → t2 → t3 → t1 → t2 → t3。
思路:让线程 m 进入某种等待状态,线程 n 执行后通知线程 m 可以执行。
实现方式
- Monitor:wait/notify(join 的本质也是 wait/notify)
- ReentrantLock
- LockSupport:park/unpark
1.2、示例
案例:要求按 t3、t2、t1 的次序执行。
线程:不加任何控制,先 start() 的线程大概率先执行。
Thread t1 = new Thread(() -> debug("执行"), "t1");
Thread t2 = new Thread(() -> debug("执行"), "t2");
Thread t3 = new Thread(() -> debug("执行"), "t3");
t1.start();
t2.start();
t3.start();
1.2.1、wait/notify
思路
-
创建锁对象 LOCK,让每个线程进入 LOCK 所关联 Monitor 的 WaitSet 进行等待。
-
定义 2 个标识,作为线程 t1 和 t2 循环条件。
(本例中线程执行顺序是 t3 -> t2 -> t1。)
-
当 t3 执行后修改 flag2,使 t2 能够执行。
-
当 t2 执行后修改 flag1,使 t1 能够执行。
static final Object LOCK = new Object(); static boolean FLAG1 = false, FLAG2 = false;
-
实现
- t1
- 循环,获取 LOCK 对象锁并进入 WaitSet(循环条件:FLAG1 为 false)。
- 当 FLAG1 == true 跳出循环,执行代码。
- t2
- 循环,获取 LOCK 对象锁并进入 WaitSet(循环条件:FLAG2 为 false)。
- 当 FLAG2 == true 跳出循环,执行代码。
- 设置 FLAG1 并唤醒 WaitSet 中的阻塞线程。
- t3:执行代码,设置 FLAG2 并唤醒 WaitSet 中的阻塞线程。
1.2.2、ReentrantLock
相比 Monitor,可以定义多个条件变量。
- 创建锁对象 reentantLock,创建 2 个条件变量。
- 让每个线程进入对应的条件变量进行等待(
await()
),直到被唤醒(signalAll()
)
1.2.3、Park/Unpark
LockSupport
对指定线程进行暂停和恢复,参考 2-park/unpark
2、管程:小结
2.1、共享问题
共享问题:多个线程访问共享变量,且执行过程不具有原子性。
- 临界区:多线程下,对共享资源执行读写操作的代码块。
- 发生竞态条件:多个线程执行临界区代码,由于代码的执行序列不同,无法预测结果。
2.2、synchronized
也称对象锁
属于重量级锁、悲观锁。
2.2.1、使用
-
语法:对象、方法签名。
synchronized(对象) { // 临界区 } 权限修饰符 synchronized [static] 返回值 方法名() { // 内部有临界区代码 }
-
- 位置:成员变量、局部变量。
- 类型:基本类型、引用类型。
2.2.2、Monitor
-
对象组成:对象头(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 | |-------------------------------------------------------|--------------------|
-
-
Monitor 结构
含义 对应线程状态 owner 锁的持有者,正在执行同步代码块 (同一时刻,Monitor 只能有一个 Owner) RUNNABLE
EntryList 由于锁已被其它线程获取,尝试获取锁失败的阻塞线程 BLOCKED
WaitSet 锁的持有者由于条件不满足,主动释放锁,进入等待状态 WAITING
-
字节码分析:monitorenter、monitorexit
2.2.3、wait/notify
- API
- wait:无限期等待、超时等待。
- notify:随机唤醒一个、唤醒所有。
- 原理:涉及 Monitor 结构的 WaitSet
Owner
线程执行条件不满足时,调用 wait(),主动释放锁并进入WaitSet
,成为WAITING
状态。- 其它线程可竞争锁,成为新的
Owner
。 - 新的
Owner
调用 notify() 或 notifyAll() 时,唤醒WaitSet
中的线程。
2.2.4、synchronized 锁优化
synchronized 是重量级锁,并发性能低。
- 加锁策略:不加锁 → 偏向锁 → 轻量级锁 → 重量级锁。
- 其它策略:自旋、锁消除、锁粗化等。
2.3、锁的活跃性
2.3.1、活跃性
- 死锁:多个线程尝试获取对方正占有的资源。
- 饥饿:优先级低的线程一直无法获得 CPU 时间片,但没有结束。
2.3.2、ReentrantLock
支持中断、超时时间、公平锁、多个条件变量。
- synchronized:底层是 C++ 实现的 Monitor。
- ReentrantLock:Java 实现。