java中的读写锁
ReentrantReadWriteLock 读写锁
之前提到锁(如Mutex和ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。
在没有读写锁支持的(Java 5之前)时候,如果需要完成上述工作就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。
一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量
ReentrantReadWriteLock其实实现的是ReadWriteLock接口
ReadWriteLock接口
1 2 3 4 5 6
| public interface ReadWriteLock { Lock readLock(); Lock writeLock(); }
|
ReentrantReadWriteLock类
构造方法
1 2 3 4 5
| ReentrantReadWriteLock()
ReentrantReadWriteLock(boolean fair)
|
常用方法摘要
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Lock ReentrantReadWriteLock.ReadLock.readLock()
Lock ReentrantReadWriteLock.WriteLock.writeLock()
int getQueueLength()
boolean isFair()
String toString()
|
ReadLock/WriteLock静态内部类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| void lock()
void lockInterruptibly()
Condition newCondition()
boolean tryLock()
boolean tryLock(long time, TimeUnit unit)
void unlock()
String toString()
|
因为ReadLock不支持条件,因此当调用了ReadLock的newCondition()方法时将会抛出UnsupportedOperationException异常。
使用ReentrantReadWriteLock的读锁以及写锁,将会遵循读读共享、写写互斥、读写互斥。
使用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| public class ReentrantReadWriteLockTest {
private static ExecutorService executorService = Executors.newCachedThreadPool();
private static ReadWriteLock lock = new ReentrantReadWriteLock();
private static Lock readLock = lock.readLock();
private static Lock writeLock = lock.writeLock();
public static void reading() { System.out.println("尝试获取读锁:" + Thread.currentThread().getId()); readLock.lock(); System.out.println("获取读锁成功:" + Thread.currentThread().getId()); try { System.out.println("开始进行读操作:" + Thread.currentThread().getId()); Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println("释放读锁成功:" + Thread.currentThread().getId()); readLock.unlock(); } }
public static void writing() { System.out.println("尝试获取写锁:" + Thread.currentThread().getId()); writeLock.lock(); System.out.println("获取写锁成功:" + Thread.currentThread().getId()); try { System.out.println("开始进行写操作:" + Thread.currentThread().getId()); } finally { System.out.println("释放写锁成功:" + Thread.currentThread().getId()); writeLock.unlock(); } } }
|
读读共享
1 2 3 4 5 6 7
| public static void main(String[] args) { for (int i = 0; i < 3; i++) { executorService.submit(() -> reading()); } executorService.shutdown(); }
|
输出
1 2 3 4 5 6 7 8 9 10 11 12
| 尝试获取读锁:12 尝试获取读锁:14 尝试获取读锁:13 获取读锁成功:14 开始进行读操作:14 获取读锁成功:12 开始进行读操作:12 获取读锁成功:13 开始进行读操作:13 释放读锁成功:12 释放读锁成功:14 释放读锁成功:13
|
读锁能被多个线程同时获取,能提高读取的效率 (虽然只用读锁时可以不进行释放,但会影响写锁的获取)
写写互斥
1 2 3 4 5 6 7
| public static void main(String[] args) { for (int i = 0; i < 3; i++) { executorService.submit(() -> writing()); } executorService.shutdown(); }
|
输出
1 2 3 4 5 6 7 8 9 10 11 12
| 尝试获取写锁:12 尝试获取写锁:13 尝试获取写锁:14 获取写锁成功:12 开始进行写操作:12 释放写锁成功:12 获取写锁成功:13 开始进行写操作:13 释放写锁成功:13 获取写锁成功:14 开始进行写操作:14 释放写锁成功:14
|
写锁同一时刻只能被一个线程获取。
读写互斥
1 2 3 4 5 6 7 8 9 10 11 12
| public static void main(String[] args) { for (int i = 0; i < 3; i++) { executorService.submit(() -> { reading(); }); executorService.submit(() -> { writing(); }); } executorService.shutdown(); }
|
输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 尝试获取读锁:12 获取读锁成功:12 开始进行读操作:12 尝试获取写锁:13 尝试获取读锁:14 尝试获取写锁:15 尝试获取读锁:16 尝试获取写锁:17 释放读锁成功:12 获取写锁成功:13 开始进行写操作:13 释放写锁成功:13 获取读锁成功:14 开始进行读操作:14 释放读锁成功:14 获取写锁成功:15 开始进行写操作:15 释放写锁成功:15 获取读锁成功:16 开始进行读操作:16 释放读锁成功:16 获取写锁成功:17 开始进行写操作:17 释放写锁成功:17
|
读的时候不能写,写的时候不能读,即获取读锁时如果写锁此时被线程持有则将等待写锁被释放,获取写锁时如果读锁此时有被线程持有则将等待读锁被释放且写锁未被持有。