之前了解读写锁的时候,只知道读读共享,读写互斥。
当线程先获取到写锁,然后再去获取读锁时,接着再释放写锁。这个过程叫做锁降级。
如果当前线程先获取读锁,然后再去获取写锁,这个时候会出现死锁的情况。
这个时候小脑袋瓜就开始嗡嗡作响了。
- 锁为什么能够降级,却不能够升级?
- 在什么场景下会出现锁降级的情况?
问题1 : 锁为什么能够降级,却不能够升级?
首先读锁可以被多个线程持有,但是写锁同一时刻只能被一个线程拥有。
按照这个理论我们来举个例子:我们知道读写锁是互斥的,读的时候不能写。
- 那么假设我有10个线程获取了读锁,这个时候10个线程都得升级成写锁,这个时候问题就出来了,你读锁还没释放啊!你要获取的写锁只能在等待队列里面排队,等待着这10个线程释放读锁,但是这10个线程释放不了,因为在获取写锁的时候已经死锁了。
这里稍微小小的做个总结:
- 读的时候不能够写,因为多线程情况下写锁会排在等待队列中读锁的后面,读都还没释放,就去获取写锁,就会造成死锁。
- 但是写的时候能读,因为写锁只能被一个线程获取,
问题2 : 在什么场景下会出现锁降级的情况?
举个例子 :
假设一个事务执行要10秒,写操作占用1秒,其他的都是读操作9秒。
在多个线程争抢读写锁的情况下:
非降级情况
- A线程拿到这个写锁就开始处理最后再释放,那么时间就是10秒,同时还可能阻塞着其他线程10秒。
- A线程只在写的时候加上写锁,其他读的情况不加锁,那么可能会出现刚释放完写锁,另一个线程就开始改数据,但是A这边可能引起数据不一致。
- A 先释放写锁,然后再获取读锁。可能就会出现别的线程抢到了写锁,A这边获取读锁的时候出现阻塞,打断了事务的进行。
锁降级的情况
当A获取完写锁并执行完1秒的写操作之后,降级成读锁,并释放写锁。
情况1:假设等待队列排在前面的是读锁线程,那么在A释放完写锁之后,等待队列中的读锁线程被传播唤醒,开始执行各自任务。
情况2:但是如果等待队列排在第一位的是写锁线程的话,不好意思还是在阻塞中。因为A还没释放完读锁,所以A还能继续执行下面操作,事务的连续性不会被打断。
当我们要保证事务的完整性,如果仅仅只用写锁去做,耗时太长,先释放写锁,再获取读锁,可能在获取读锁的时候,别的线程已经在抢写锁了,而你读锁只能等待!这个时候你的事务不仅被打断了,可能执行的时间更长。
而在锁降级的帮助下,情况1的优势发挥了出来。
总结
- 在耗时长的事务中,锁降级的优势:
- 使写锁占用时间缩短.
- 线程并发的程度增高
- 并且保证了事务不被打断。
另外附上验证的代码块:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockTest {
private Logger logger = LoggerFactory.getLogger(ReadWriteLockTest.class);
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
/**
* 这里仅仅是想知道锁重入的情况,是不是这个时候加入的锁会到等待队列里面排队。
*/
public void queryData() {
try {
Thread.sleep(500);
readLock.lock();
logger.info("查询数据完成.");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
}
public void test3() throws Exception {
// 开始锁降级
writeLock.lock();
logger.info("主线程抢到写锁...");
// 这里的休眠是为了让下面线程能在预想的情况下加入等待队列.
Thread.sleep(500);
// 这里就是假设等待队列里面排在前面的是读锁线程
processReadLock(1); // 这里可以和下面processWriteLock对调
processWriteLock(2);
Thread.sleep(500);
// 开始锁降级
readLock.lock(); // A 降级开始
// 锁降级完成
writeLock.unlock();
logger.info("主线程释放写锁");
queryData();
readLock.unlock(); // A 降级结束
logger.info("主线程读锁释放");
logger.info("过程结束..");
}
private void processWriteLock(int threadIndex) {
new Thread(() -> {
logger.info("线程" + threadIndex + " 写锁开始竞争,阻塞中.");
writeLock.lock();
logger.info("线程" + threadIndex + " 写锁执行中..");
writeLock.unlock();
logger.info("线程" + threadIndex + " 写锁释放..");
}).start();
}
private void processReadLock(int threadIndex) {
new Thread(() -> {
logger.info("线程" + threadIndex + " 读锁开始竞争,阻塞中.");
readLock.lock();
logger.info("线程" + threadIndex + " 读锁执行中..");
readLock.unlock();
logger.info("线程" + threadIndex + " 读锁释放..");
}).start();
}
public static void main(String[] args) throws Exception {
ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();
readWriteLockTest.test3();
}
}
打印结果:
主线程抢到写锁...
线程1 读锁开始竞争,阻塞中. // 这里等待队列中排在第一个的是读锁.
线程2 写锁开始竞争,阻塞中. // 队列中的第二个是写锁
主线程释放写锁 // 主线程先释放写锁,但是还持有着读锁
线程1 读锁执行中.. // 队列中的读锁已经被唤醒
线程1 读锁释放.. //读锁处理完毕.
查询数据完成. // 主线程的读锁也处理完毕了
主线程读锁释放
过程结束.. // 主线程处理完毕
线程2 写锁执行中.. // 这个时候,等待队列中的写锁被触发
线程2 写锁释放..
我这里大概描述一下我想预测的几种情况:
主线程先抢到写锁,然后降级成为读锁。
锁降级成功之后:
- 主线程此刻是处于读锁线程,那么等待队列中的读锁是否会被唤醒? // 答案是会被唤醒
- 如果等待队列中排队的是写线程,在我主线程写锁先释放的时候,会不会抢到?// 答案是不会,因为主线程的读锁还没释放。
这里测试下来,就是在锁降级的情况下:
- 如果等待队列中最前面的是写线程,必须等主线程的读写锁都释放的情况下,才会交给等待队列。
- 如果等待队列中最前面的是读线程,那么主线程释放完读锁,就会被唤醒。
如果上述观点有误,请留言探讨,共同进步,谢谢观看。