这是我参与更文挑战的第21天,活动详情查看:更文挑战
读写锁
读写锁由读锁 和 写锁 两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。所以,读写锁适用于能明确区分读操作和写操作的场景。
读写锁的工作原理:
- 当 写锁 没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为 读锁 是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。
- 但是,一旦 写锁 被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。
- 所以说,写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有。
- 基于读写锁的工作原理,我们可以发现,读写锁在读多写少的场景,能发挥出优势。
读优先锁
读优先锁期望的是,读锁能被更多的线程持有,以便提高读线程的并发性。它的工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 仍然可以成功获取读锁,最后直到读线程 A 和 C 释放读锁后,写线程 B 才可以成功获取读锁。如下图:
写优先锁
写优先锁是优先服务写线程,其工作方式是:当读线程 A 先持有了读锁,写线程 B 在获取写锁的时候,会被阻塞,并且在阻塞过程中,后续来的读线程 C 获取读锁时会失败,于是读线程 C 将被阻塞在获取读锁的操作,这样只要读线程 A 释放读锁后,写线程 B 就可以成功获取读锁。如下图:
读优先锁和写优先锁导致的线程饥饿
- 线程饥饿:线程因无法访问所需资源而无法执行下去的情况。
读优先锁对于读线程并发性更好,但也不是没有问题。我们试想一下,如果一直有读线程获取读锁,那么写线程将永远获取不到写锁,这就造成了写线程饥饿的现象。写优先锁可以保证写线程不会饿死,但是如果一直有写线程获取写锁,读线程也会被饿死。
既然不管优先读锁还是写锁,对方可能会出现饿死问题,那么我们就不偏袒任何一方,搞个公平读写锁。公平读写锁比较简单的一种方式是:用队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可,这样读线程仍然可以并发,也不会出现饥饿的现象。
ReentrantReadWriteLock
Java 通过ReentrantReadWriteLock
类来实现读写锁,其中读锁和写锁的互斥机制由JVM控制。以下为ReentrantReadWriteLock
类的简单使用。
/**
* 创建一个读写锁
* 它是一个读写融为一体的锁,在使用的时候,需要转换
*/
private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 获取读锁
rwLock.readLock().lock();
// 释放读锁
rwLock.readLock().unlock();
// 创建一个写锁
rwLock.writeLock().lock();
// 写锁 释放
rwLock.writeLock().unlock();
复制代码