并发4️⃣管程⑤锁粒度、活跃性、ReentrantLock
1、锁的粒度
若对象的多个方法不会操作同一个变量。
细分锁的粒度,以提高并发性。
1.1、示例
示例:House 有 study 和 sleep 两个方法(互不影响)
-
House 类:执行方法时锁住当前对象。
public class House { public void sleep() { synchronized (this) { LogUtils.debug("sleeping..."); sleepSeconds(2); LogUtils.debug("OK"); } } public void study() { synchronized (this) { LogUtils.debug("studying..."); sleepSeconds(2); LogUtils.debug("OK"); } } }
-
测试:
-
开启两个线程,分别调用两个方法。
House house = new House(); new Thread(() -> house.sleep(), "t1").start(); new Thread(() -> house.study(), "t2").start();
-
t1 执行 sleep() 时会导致 t2 阻塞。
-
1.2、优化
做法
-
定义两个成员变量,作为锁对象。
-
方法中的
synchronized(this)
改为对应的锁对象。public class House { private final Object sleepRoom = new Object(); private final Object studyRoom = new Object(); public void sleep() { synchronized (sleepRoom) {...} } public void study() { synchronized (studyRoom) {...} } }
测试
t1 和 t2 同时执行,互不影响。
1.3、死锁问题
细分锁的粒度可能会导致死锁发生。
示例:对象 obj 有 m1() 和 m2() 两个方法,方法中 synchronized
相应的锁对象。
- 某一时刻,线程 t1 正在执行 m1(),线程 t2 正在执行 m2()。
- t1 要调用 m2(),且 t2 要调用 m1(),但锁都未被释放。
- 两个线程都占用锁并等待对方释放锁,导致程序无法运行。
2、活跃性
2.1、死锁(❗)
简单来说,死锁就是多个线程尝试获取对方正占有的资源。
2.1.1、检测方式
- 先 jps 获得进程 id,再 jstack 定位死锁。
- jconsole 可视化工具。
2.1.2、示例:筷子问题
有 5 个人围成一桌,每人左右各有 1 支筷子。
-
执行:需要同时获得左右筷子,才能吃饭。
-
阻塞:若筷子被身边的人获得,需要等待释放。
-
死锁:恰好每人拿到 1 根筷子,且都在等旁边的人释放。
Java 实现
-
Chopstick 类
class Chopstick { private String name; public Chopstick() { } }
-
Person 类:继承 Thread 类
-
eat():打印输出语句,并睡眠 1 秒。
-
run():循环。先后对 left 和 right 加锁,执行 eat()。
public class Person extends Thread { private Chopstick left, right; public Person(String name, Chopstick left, Chopstick right) { // 线程名 setName(name); this.left = left; this.right = right; } @Override public void run() { while (true) { synchronized (left) { synchronized (right) { eat(); } } } } private void eat() { LogUtils.debug("eating..."); SleepUtils.sleepSeconds(1); } }
-
测试
-
代码:5 人 5 筷子。
Chopstick c1 = new Chopstick(); Chopstick c2 = new Chopstick(); Chopstick c3 = new Chopstick(); Chopstick c4 = new Chopstick(); Chopstick c5 = new Chopstick(); new Person("老大", c1, c2).start(); new Person("二货", c2, c3).start(); new Person("张三", c3, c4).start(); new Person("李四", c4, c5).start(); new Person("王五", c5, c1).start();
-
结果:程序运行一段时间后不再执行,即死锁。
2.2、饥饿
- OS:某个进程一直得不到资源。
- Java:优先级低的线程一直无法获得 CPU 时间片,但没有结束(仍处于就绪状态)。
2.3、说明
活跃性问题(死锁、活锁、饥饿),都可用 ReentrantLock 来解决。
3、ReentrantLock(❗)
ReentrantLock:可重入锁
3.1、说明
二者使用基本相同,可对比理解。
- synchronized:底层是 C++ 实现的 Monitor。
- ReentrantLock:Java 实现。
3.1.1、对比
ReentrantLock 支持中断、超时时间、公平锁、多个条件变量。
synchronized | ReentrantLock | |
---|---|---|
含义 | 对象锁(重量级) | 可重入锁 |
可重入 | ✔ | ✔ |
可中断 | 进入 EntryList 的线程会一直阻塞并尝试获取锁 | lockInteruptibly() 可被其它线程中断阻塞状态 |
超时时间 | 进入 EntryList 的线程会无限期尝试获取锁,直到成功竞争锁 | trywait() 可设置超时时间,超过指定时间后不再尝试获取锁 |
公平锁 | 不支持,Owner 唤醒 EntryList 的所有阻塞线程进行竞争 | 支持,先进入 EntryList 的先成为新的 Owner(FIFO) |
条件变量 | 单个,即 WaitSet | await() 支持多个,用于表示不同的等待条件 |
3.1.2、语法
-
synchronized
-
步骤:创建锁对象、加锁。
-
在字节码层面自动加锁和解锁,monitorenter 和 monitorexit。
Object lock = new Object(); synchronized(lock){ // 临界区 }
-
-
ReentrantLock
-
步骤:创建锁对象、加锁、释放锁。
-
需要手动调用加锁和解锁方法,lock() 和 unlock()。
-
需结合 try 代码块,unlock() 放在 finally 块首行。
ReentrantLock reentrantLock = new ReentrantLock(); reentrantLock.lock(); try { // 临界区 } finally { reentrantLock.unlock(); }
-
3.2、特点
3.2.1、可重入
- 不可重入:线程 t1 获得对象 obj 的锁,在释放锁之前无法再次获取锁。
- 可重入:t1 在释放 obj 锁之前可再次获取锁。
测试
对 reentrantLock 对象加锁,且调用另一个会对 reentrantLock 加锁的方法。
-
代码
ReentrantLock reentrantLock = new ReentrantLock(); public void reentry() { reentrantLock.lock(); try { LogUtils.debug("调用 reentry()"); m1(); } finally { reentrantLock.unlock(); } } private void m1() { reentrantLock.lock(); try { LogUtils.debug("调用 m1()"); m2(); } finally { reentrantLock.unlock(); } } private void m2() { reentrantLock.lock(); try { LogUtils.debug("调用 m2()"); } finally { reentrantLock.unlock(); } }
-
测试:成功进入 3 个方法。
3.2.2、可中断
lockInterruptibly()
-
lockInterruptibly() 方法可被中断,因此会抛出 InterruptedException 异常。
-
其余步骤与 lock() 相同。
try { // 加锁 reentrantLock.lockInterruptibly(); try { // 临界区 LogUtils.debug("得到锁"); } finally { // 释放锁 reentrantLock.unlock(); LogUtils.debug("释放锁"); } } catch (InterruptedException e) { e.printStackTrace(); LogUtils.debug("尝试获取锁时被中断"); }
3.2.3、超时时间
尝试获取锁时,超过指定时间则不再尝试获取。
返回值为 boolean
-
tryLock():不等待。
-
tryLock(long, TimeUnit):指定时间单位,等待 long 单位时间。
if (!reentrantLock.tryLock()) { LogUtils.debug("超时,不再尝试获取"); } try { LogUtils.debug("得到锁"); } finally { reentrantLock.unlock(); }
解决 2.1.2 死锁问题
-
Chopstick 继承 ReentrantLock 类
-
将 synchronized 改成 ReentrantLock 的相关方法。
-
尝试获取 left,失败(返回 false)则进入下一轮循环。
-
已获取 left,尝试获取 right
-
成功:拿到两幅侉子,调用 eat() 吃饭。
-
失败:执行外层的 finally,释放 left(相当于放下左筷子)。
public class Person extends Thread { // 省略其它 @Override public void run() { while (true) { // 尝试获取左筷子 if (left.tryLock()) { try { // 尝试获取右筷子 if (right.tryLock()) { try { eat(); } finally { right.unlock(); } } } finally { left.unlock(); } } } } } class Chopstick extends ReentrantLock {}
-
-
-
测试:不会死锁,也不会出现饥饿问题。
3.2.4、公平锁
ReentrantLock 默认是非公平锁。
在构造方法的参数列表,可设置 true 表示开启公平锁。
-
空参:nonFair
-
boolean:true 表示开启
public ReentrantLock() { sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
说明
- 公平锁可解决饥饿问题。
- 公平锁会降低并发性,使用上不如
trylock()
。
3.2.5、条件变量
注:使用方式相同,细节不同。
Object | ReentrantLock | |
---|---|---|
使用前提 | 获得对象锁(synchronized) | 获取条件变量(conditionObject) |
条件变量个数 | 一个,即 WaitSet | 多个(细分不同条件,) |
等待 | wait() | await() |
唤醒 | notify() / notifyAll() | signal() / signalAll() |