前面我们介绍了管程和信号量这两个同步原语在 Java 语言中的实现,理论上用这两个同步原语中任何一个都可以解决所有的并发问题。那 Java SDK 并发包里为什么还有很多其他的工具类呢?原因很简单:分场景优化性能,提升易用性
1.读写锁:ReadWriteLock
读写锁:
- 允许多个线程同时读共享变量
- 只允许一个线程写共享变量
- 如果一个线程正在执行写操作,此时限制读线程读共享变量
1.1.互斥锁与读写锁的重要区别以及适用场景
- 读写锁允许多个线程同时读共享变量,而互斥锁不允许
- 在读多写少的场景,读写锁的性能明显优于互斥锁
2.读写锁的注意点
2.1.关于锁升级和锁降级
锁升级:如果当前线程已经拿到了读锁,此时如果再去拿写锁(升级为写锁),这个过程称为锁升级,但是ReadWriteLock 不支持锁升级,此时会发生死锁,示例如下:
final ReadWriteLock rwl = new ReentrantReadWriteLock();
// 读锁
final Lock readLock = rwl.readLock();
// 写锁
final Lock writeLock = rwl.writeLock();
try {
//先拿到读锁
readLock.lock();
//再拿到写锁,此时会发生锁升级以及死锁问题,类似mysql的读锁
writeLock.lock();
System.out.println("拿到写锁拉");
System.out.println("锁降级拉");
for (int i = 0; i < time; i++) {
System.out.println("请求写锁:"+i+"秒");
Thread.sleep(1000 * 1);
}
}finally {
writeLock.unlock();
readLock.unlock();
}
}
锁降级:如果当前线程拿到了写锁,此时如果再去上读锁(降级为读锁),这个过程称为锁降级,ReadWriteLock是支持的
其实读写锁的升级和降级或者说读写锁的上锁操作,其实也对应于MySQL的读锁和写锁:
xx.readLock()
操作,其实就是相当于MySQL的读锁,只允许当前线程和其他线程上读锁,但是不允许上写锁,此时如果当前线程上了读锁又去上写锁(锁升级),就会出现死锁的场景
xx.writeLock
操作,其实就是相当于MySQL的写锁,只允许当前线程(事务)上进行读写操作(上读锁和写锁),其余线程如果请求上写锁或读锁时,就会发送阻塞操作直至取得写锁的线程释放写锁。
总结一下:
-
MySQL的读锁又上写锁,就是ReadWriteLock的锁升级的场景,但是ReadWriteLock不支持这个场景,并且会导致死锁
-
MySQL的写锁的功能,就是ReadWriteLock的锁降级的过程
-
ReadWriteLock的写锁已被申请之后,读锁只能等待写锁都被释放才能去上写锁,而读锁被申请后,其余线程还可以再次上读锁
-
读写锁中的每个线程只能释放自己线程申请的锁,否则释放锁的线程会抛出异常:
