今天来说说Java 的读写锁-ReadWriteLock,ReadWriteLock是一个接口,实现类是ReentrantReadWriteLock,看着名字的翻译就是可重入读写锁。
为什么Java会搞了那么多种类的锁,因为不同的场景需要做不同的适配来达到性能和使用的最优,而读写锁的使用场景就是读多写少。
读写锁是什么?
读写锁就是分了两种情况,一种是读时的锁,一种是写时的锁,它允许多个线程同时读共享变量,但是只允许一个线程写共享变量,当写共享变量的时候也会阻塞读的操作。这样在读的时候就不会互斥,提高读的效率。
可重入锁是什么?
可重入锁指的是在同一个线程内如果你的外层函数已经获得了锁,那么当你的内层函数也能获取锁,也就是通过一个线程再次进入同步代码块时可以获得自己已经获得的锁,而不可重入则反之。看下简单的示例
public void doSth(){ //外层函数 lock.lock(); do(); //内层函数 lock.unlock(); } public void do(){ lock.lock(); //do something lock.unlock(); }
可重入锁这样使用上面代码是没问题的,如果不可重入锁这样的调用是不允许的。
来看看Javadoc中ReentrantReadWriteLock的示例,主要说的就是处理一个获取一个缓存数据的示例
class CachedData { Object data; volatile boolean cacheValid; final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); //先获取读锁 if (!cacheValid) { //如果缓存失效,那表示需要重新去获取数据写入缓存,所以就得变成写锁 // Must release read lock before acquiring write lock (在获取写锁前必须释放读锁) rwl.readLock().unlock(); rwl.writeLock().lock(); try { // Recheck state because another thread might have // acquired write lock and changed state before we did. // (重新检查状态,因为有可能别的线程已经在当前线程获取写锁时已经更新了缓存) if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock // (通过在释放写锁之前获取读锁来降级) rwl.readLock().lock(); } finally { //(释放写锁,仍持有写锁) rwl.writeLock().unlock(); // Unlock write, still hold read } } try { use(data); } finally { rwl.readLock().unlock(); } } }
可以看出使用还是简单的,相对于ReentrantLock来说就是多了个角色区别一个是读锁一个是写锁。
读写锁的升降级
上面代码有提到
Must release read lock before acquiring write lock (在获取写锁前必须释放读锁)
也就是说 读写锁不允许锁的升级,不能直接从读锁升级到写锁。 如果读锁还没有释放,此时获取写锁,会导致写锁永久等待,最终导致相关线程都阻塞,GG。切记不可这样使用。
但是锁的降级是允许的
Downgrade by acquiring read lock before releasing write lock(通过在释放写锁之前获取读锁来降级)
也就是说在释放写锁之前可以获取读锁来达到锁的降级!
读写锁还有一点不同就是写锁是支持条件变量的也就是支持newCondition
,而读锁是不支持条件变量的,如果读锁调用newCondition
会抛UnsupportedOperationException
。
读写锁实现了java.util.concurrent.locks.Lock
接口,所以tryLock()、lockInterruptibly()
等方法都是支持的,并且也支持公平锁和非公平锁的模式,底层是也是基于AbstractQueuedSynchronizer
实现的,所以对于公平和非公平锁的实现可以参见面试官:说说Java中的信号量?Semaphore 我的这篇文章