ReentrantReadWriteLock是JDK5中提供的读写分离锁。读写分离锁可以有效的帮助减少锁的竞争,以此来提升系统的性能。用锁分离的机制来提升性能也非常好理解,比如线程A,B,C进行写操作,D,E,F进行读操作,如果使用ReentrantLock或者synchronized关键字,这些线程都是串行执行的,即每次都只有一个线程做操作。但是当D进行读操作时,E,F都需要等待锁,由于读不会对数据的完整性造成破坏,因此这种等待是不合理的。
在这种情况下,读写锁运行多个线程同时读,是的D,E,F之间真正的并行。但是由于需要考虑数据的完整性,写写操作和读写操作还是需要互相等待和持有锁的,读写锁的约束情况如下:
❤ 读-读不互斥:读读之间不阻塞;
❤ 读-写互斥:读阻塞写,写也会阻塞读;
❤ 写-写互斥:写写阻塞;
源码:
这是ReentrantReadWriteLock的两个构造函数:
1 public ReentrantReadWriteLock() {
2 this(false);
3 }
4
5 public ReentrantReadWriteLock(boolean fair) {
6 sync = fair ? new FairSync() : new NonfairSync();
7 readerLock = new ReadLock(this);
8 writerLock = new WriteLock(this);
9 }
可以看出,读写锁也可以构造公平和非公平锁,默认的是非公平锁。
看下面的例子:
1 public class ReadWriteLock {
2 private static Lock lock = new ReentrantLock();
3 private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
4 private static Lock readLock = readWriteLock.readLock();
5 private static Lock writeLock = readWriteLock.writeLock();
6 private int value;
7
8 //模拟读操作
9 public Object handleRead(Lock lock) throws InterruptedException{
10 try {
11 lock.lock();
12 System.out.println("获取读锁 :" + System.currentTimeMillis());
13 Thread.sleep(1000);
14 return value;
15 } finally {
16 lock.unlock();
17 }
18 }
19
20 //模拟写操作
21 public void handleWrite(Lock lock,int index) throws InterruptedException {
22 try {
23 lock.lock();
24 System.out.println("获取写锁:" + System.currentTimeMillis());
25 Thread.sleep(1000);
26 value = index;
27 }finally {
28 lock.unlock();
29 }
30 }
31
32 //测试
33 public static void main(String[] args){
34 ReadWriteLock demo = new ReadWriteLock();
35 //读线程
36 Runnable readRunnable = new Runnable() {
37 @Override
38 public void run() {
39 try {
40 demo.handleRead(readLock);
41 //demo.handleRead(lock);
42 } catch (InterruptedException e) {
43 e.printStackTrace();
44 }
45 }
46 };
47 //写线程
48 Runnable writeRunnable = new Runnable() {
49 @Override
50 public void run() {
51 try {
52 demo.handleWrite(writeLock,new Random().nextInt());
53 //demo.handleWrite(lock,new Random().nextInt());
54 } catch (InterruptedException e) {
55 e.printStackTrace();
56 }
57 }
58 };
59
60 for (int i = 0;i < 10;i++){
61 new Thread(readRunnable).start();
62 }
63
64 for (int i = 0;i < 10;i++){
65 new Thread(writeRunnable).start();
66 }
67
68 }
69
70 }
输出结果:
1 获取读锁 :1537857549893
2 获取读锁 :1537857549893
3 获取读锁 :1537857549893
4 获取读锁 :1537857549893
5 获取读锁 :1537857549893
6 获取读锁 :1537857549893
7 获取写锁:1537857550893
8 获取写锁:1537857551893
9 获取写锁:1537857552893
10 获取写锁:1537857553893
11 获取读锁 :1537857554893
12 获取读锁 :1537857554893
13 获取读锁 :1537857554893
14 获取读锁 :1537857554893
15 获取写锁:1537857555893
16 获取写锁:1537857556893
17 获取写锁:1537857557893
18 获取写锁:1537857558893
19 获取写锁:1537857559893
20 获取写锁:1537857560893
上述的我们分别让读和写的线程都等待1S,当我们使用读写锁时,可以有输出时间的时间戳看出,整个的过程耗时11秒;也可以从获得读锁的时间戳看出,获取读锁是可以同时获得的,表明读-读不互斥,可以并行的;看输出结果的第6,7行和10,11行和14,15行,可以看出这些获得锁的时间差都为1S,表明了读-写互斥和写-读互斥;再看获取写锁的时间戳,每次获得写锁都是互斥的,每次都间隔1S得到。
将上述例子,第41和53行代码放开,注释第40和52行代码,执行得到结果:
1 获取读锁 :1537859337210
2 获取读锁 :1537859338210
3 获取读锁 :1537859339210
4 获取读锁 :1537859340210
5 获取读锁 :1537859341210
6 获取读锁 :1537859342210
7 获取读锁 :1537859343210
8 获取读锁 :1537859344210
9 获取读锁 :1537859345210
10 获取读锁 :1537859346210
11 获取写锁:1537859347210
12 获取写锁:1537859348210
13 获取写锁:1537859349210
14 获取写锁:1537859350210
15 获取写锁:1537859351211
16 获取写锁:1537859352211
17 获取写锁:1537859353211
18 获取写锁:1537859354211
19 获取写锁:1537859355211
20 获取写锁:1537859356211
修改上述代码后,我们是使用重入锁来实现的,可以看出整个过程耗时20秒,再看每次获取锁的时间戳,我们可以得出不论什么线程都是互斥的。
由两个输出结果可以看出,读写锁对于读比较多的应用场景性能提升较大。
参考:《Java高并发程序设计》 葛一鸣 郭超 编著: