ReentrantReadWriteLock是JDK5中提供的读写分离锁。读写分离锁可以有效的帮助减少锁的竞争,以此来提升系统的性能。用锁分离的机制来提升性能也非常好理解,比如线程A,B,C进行写操作,D,E,F进行读操作,如果使用ReentrantLock或者synchronized关键字,这些线程都是串行执行的,即每次都只有一个线程做操作。但是当D进行读操作时,E,F都需要等待锁,由于读不会对数据的完整性造成破坏,因此这种等待是不合理的。
在这种情况下,读写锁运行多个线程同时读,是的D,E,F之间真正的并行。但是由于需要考虑数据的完整性,写写操作和读写操作还是需要互相等待和持有锁的,读写锁的约束情况如下:
❤ 读-读不互斥:读读之间不阻塞;
❤ 读-写互斥:读阻塞写,写也会阻塞读;
❤ 写-写互斥:写写阻塞;
源码:
这是ReentrantReadWriteLock的两个构造函数:
11 public ReentrantReadWriteLock() { 22 this(false); 33 } 44 55 public ReentrantReadWriteLock(boolean fair) { 66 sync = fair ? new FairSync() : new NonfairSync(); 77 readerLock = new ReadLock(this); 88 writerLock = new WriteLock(this); 99 }
可以看出,读写锁也可以构造公平和非公平锁,默认的是非公平锁。
看下面的例子:
1 1 public class ReadWriteLock { 2 2 private static Lock lock = new ReentrantLock(); 3 3 private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); 4 4 private static Lock readLock = readWriteLock.readLock(); 5 5 private static Lock writeLock = readWriteLock.writeLock(); 6 6 private int value; 7 7 8 8 //模拟读操作 9 9 public Object handleRead(Lock lock) throws InterruptedException{ 1010 try { 1111 lock.lock(); 1212 System.out.println("获取读锁 :" + System.currentTimeMillis()); 1313 Thread.sleep(1000); 1414 return value; 1515 } finally { 1616 lock.unlock(); 1717 } 1818 } 1919 2020 //模拟写操作 2121 public void handleWrite(Lock lock,int index) throws InterruptedException { 2222 try { 2323 lock.lock(); 2424 System.out.println("获取写锁:" + System.currentTimeMillis()); 2525 Thread.sleep(1000); 2626 value = index; 2727 }finally { 2828 lock.unlock(); 2929 } 3030 } 3131 3232 //测试 3333 public static void main(String[] args){ 3434 ReadWriteLock demo = new ReadWriteLock(); 3535 //读线程 3636 Runnable readRunnable = new Runnable() { 3737 @Override 3838 public void run() { 3939 try { 4040 demo.handleRead(readLock); 4141 //demo.handleRead(lock); 4242 } catch (InterruptedException e) { 4343 e.printStackTrace(); 4444 } 4545 } 4646 }; 4747 //写线程 4848 Runnable writeRunnable = new Runnable() { 4949 @Override 5050 public void run() { 5151 try { 5252 demo.handleWrite(writeLock,new Random().nextInt()); 5353 //demo.handleWrite(lock,new Random().nextInt()); 5454 } catch (InterruptedException e) { 5555 e.printStackTrace(); 5656 } 5757 } 5858 }; 5959 6060 for (int i = 0;i < 10;i++){ 6161 new Thread(readRunnable).start(); 6262 } 6363 6464 for (int i = 0;i < 10;i++){ 6565 new Thread(writeRunnable).start(); 6666 } 6767 6868 } 6969 7070 }
输出结果:
1 1 获取读锁 :1537857549893 2 2 获取读锁 :1537857549893 3 3 获取读锁 :1537857549893 4 4 获取读锁 :1537857549893 5 5 获取读锁 :1537857549893 6 6 获取读锁 :1537857549893 7 7 获取写锁:1537857550893 8 8 获取写锁:1537857551893 9 9 获取写锁:1537857552893 1010 获取写锁:1537857553893 1111 获取读锁 :1537857554893 1212 获取读锁 :1537857554893 1313 获取读锁 :1537857554893 1414 获取读锁 :1537857554893 1515 获取写锁:1537857555893 1616 获取写锁:1537857556893 1717 获取写锁:1537857557893 1818 获取写锁:1537857558893 1919 获取写锁:1537857559893 2020 获取写锁:1537857560893
上述的我们分别让读和写的线程都等待1S,当我们使用读写锁时,可以有输出时间的时间戳看出,整个的过程耗时11秒;也可以从获得读锁的时间戳看出,获取读锁是可以同时获得的,表明读-读不互斥,可以并行的;看输出结果的第6,7行和10,11行和14,15行,可以看出这些获得锁的时间差都为1S,表明了读-写互斥和写-读互斥;再看获取写锁的时间戳,每次获得写锁都是互斥的,每次都间隔1S得到。
将上述例子,第41和53行代码放开,注释第40和52行代码,执行得到结果:
1 1 获取读锁 :1537859337210 2 2 获取读锁 :1537859338210 3 3 获取读锁 :1537859339210 4 4 获取读锁 :1537859340210 5 5 获取读锁 :1537859341210 6 6 获取读锁 :1537859342210 7 7 获取读锁 :1537859343210 8 8 获取读锁 :1537859344210 9 9 获取读锁 :1537859345210 1010 获取读锁 :1537859346210 1111 获取写锁:1537859347210 1212 获取写锁:1537859348210 1313 获取写锁:1537859349210 1414 获取写锁:1537859350210 1515 获取写锁:1537859351211 1616 获取写锁:1537859352211 1717 获取写锁:1537859353211 1818 获取写锁:1537859354211 1919 获取写锁:1537859355211 2020 获取写锁:1537859356211
修改上述代码后,我们是使用重入锁来实现的,可以看出整个过程耗时20秒,再看每次获取锁的时间戳,我们可以得出不论什么线程都是互斥的。
由两个输出结果可以看出,读写锁对于读比较多的应用场景性能提升较大。
参考:《Java高并发程序设计》 葛一鸣 郭超 编著: