保护性暂挂模式
更新: 3/3/2025 字数: 0 字 时长: 0 分钟
对于锁的正确理解
锁是每一个Java程序员都必然会接触到的内容,但是如何正确的理解锁是一个问题。
个人认为,对于锁的正确理解是:锁是用来保护锁住的数据,一旦数据被上锁,则在并发场景下该数据不会出现问题
举个例子
常见的对象锁,我们讲对象锁后,其他线程对该对有的操作均要等待解锁,但此时若对相同类的不同实例化对象进行操作,则可以进行操作。
同样的,如果我们想要让一个类的所有对象均高并发下稳定,则对对象对应的Class进行上锁(缺点为所有针对该类的操作全部变为串行,相当于直接地图炮)
死锁的形成
死锁的概念也很简单,就是两个线程互相请求对方持有的锁。但是我们如何认知死锁并且预防死锁呢?
一个简单的判断方式是,一旦一个线程(往往是一次网络请求)持有一个以上的锁时,就可能存在死锁问题。(因为复数个锁才可能会出现相互请求)
常见的解决方式
- 让锁获取不到就不再等待/为等待锁上一个最大的限制时间:这个操作可以通过ReentrantLock类的tryLock方法实现,缺点为无法保证一个操作一定会完成,可以通过自旋的方式来优化,但性能也会因此降低
- 一次性获取两个锁:我们可以通过一个中间件来放入所有可能上锁的对象,然后上锁直接等价位取出中间件中的对象,同时为了防止中间件操作出现问题,我们可能还需要对中间件采取上锁,缺点为引入中间件,性能降低,重量提升
- 保证锁顺序获取:如果我们让复数个锁均按一个固定的顺序去取的话则可防止死锁,缺点为仅对两个线程申请的复数个锁完全一样时才生效,条件较难达成
保护性暂挂模式
针对我们拿不到锁采取自旋的方案,由于高并发环境下大量自旋对CPU压力过大,我们可以采用挂起的方式,这就是我们保护性暂挂模式的理念
java
public class GuardedQueue {
private final Queue<Integer> sourceList;
public GuardedQueue() {
this.sourceList = new LinkedBlockingQueue<>();
}
public synchronized Integer get() {
while (sourceList.isEmpty()) {
try {
wait(); //若拿不到对象,则直接挂起,等待被唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return sourceList.peek();
}
public synchronized void put(Integer e) {
sourceList.add(e);
notifyAll(); //当对象放回队列后,唤醒所有使用GuardedQueue实例的wait线程
}
}
拓展——监控器
由于保护挂起模式实际上是使用while对一个条件监控完成了自旋,因此我们可以基于保护性暂挂模式的设计方案,我们可以自己做一个微型的监听器用于监听一个实践
java
public class Listener {
private volatile Boolean condition=false;
public synchronized Integer get() {
while (!condition) {
try {
wait(); //若拿不到对象,则直接挂起,等待被唤醒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//.......剩余的操作
}
public setCondition(Boolean condition){
this.condition=condition;
if(condition){
notifyAll();
}
}
}