19.0 第19章 同步和协作工具类 19.1 读写锁ReentrantReadWriteLock
第19章 同步和协作工具类
我们在15.3节实现了线程的一些基本协作机制,那是利用基本的wait/notify实现的。我们提到,Java并发包中有一些专门的同步和协作工具类,本章,我们就来探讨它们。具体工具类包括:
- 读写锁ReentrantReadWriteLock。
- 信号量Semaphore。
- 倒计时门栓CountDownLatch。
- 循环栅栏CyclicBarrier。
此外,有一个实现线程安全的特殊概念:线程本地变量ThreadLocal,本章也会进行介绍。
与第15章介绍的显式锁和显式条件类似,除了ThreadLocal外,这些同步和协作类都是基于AQS实现的。在一些特定的同步协作场景中,相比使用最基本的wait/notify以及显式锁/条件,它们更为方便,效率更高。下面,我们就来探讨它们的基本概念、用法、用途和基本原理。
19.1 读写锁ReentrantReadWriteLock
之前章节我们介绍了两种锁:synchronized和显式锁ReentrantLock,对于同一受保护对象的访问,无论是读还是写,它们都要求获得相同的锁。在一些场景中,这是没有必要的,多个线程的读操作完全可以并行,在读多写少的场景中,让读操作并行可以明显提高性能。
怎么让读操作能够并行,又不影响一致性呢?答案是使用读写锁。在Java并发包中,接口ReadWriteLock表示读写锁,主要实现类是可重入读写锁ReentrantReadWriteLock。ReadWriteLock的定义为:
1 | public interface ReadWriteLock { |
通过一个ReadWriteLock产生两个锁:一个读锁,一个写锁。读操作使用读锁,写操作使用写锁。需要注意的是,只有“读-读”操作是可以并行的,“读-写”和“写-写”都不可以。只有一个线程可以进行写操作,在获取写锁时,只有没有任何线程持有任何锁才可以获取到,在持有写锁时,其他任何线程都获取不到任何锁。在没有其他线程持有写锁的情况下,多个线程可以获取和持有读锁。
ReentrantReadWriteLock是可重入的读写锁,它有两个构造方法,如下所示:
1 | public ReentrantLock() |
fire表示是否公平,如果不传递则是false,含义与16.2节介绍的类似,就不赘述了。
我们看个读写锁的应用,使用ReentrantReadWriteLock实现一个缓存类MyCache,如代码清单19-1所示。
1 | public class MyCache { |
代码比较简单,就不赘述了。读写锁是怎么实现的呢?读锁和写锁看上去是两个锁,它们是怎么协调的?具体实现比较复杂,我们简述下其思路。
内部,它们使用同一个整数变量表示锁的状态,16位给读锁用,16位给写锁用,使用一个变量便于进行CAS操作,锁的等待队列其实也只有一个。
写锁的获取,就是确保当前没有其他线程持有任何锁,否则就等待。写锁释放后,也就是将等待队列中的第一个线程唤醒,唤醒的可能是等待读锁的,也可能是等待写锁的。
读锁的获取不太一样,首先,只要写锁没有被持有,就可以获取到读锁,此外,在获取到读锁后,它会检查等待队列,逐个唤醒最前面的等待读锁的线程,直到第一个等待写锁的线程。如果有其他线程持有写锁,获取读锁会等待。读锁释放后,检查读锁和写锁数是否都变为了0,如果是,唤醒等待队列中的下一个线程。