读写锁ReentrantReadWriteLock用法详解

读写锁ReentrantReadWriteLock用法详解

前言

业务开发中我们可能涉及到读写操作。
面对写和读,对于数据同步,在使用Lock锁和synchronized关键字同步数据时候,对于读读而言,两个线程也需要争抢锁,此时额外争抢锁是没有意义的,造成性能损耗,写的时候,不能读,没有写的时候,读线程不能互斥。

对于Lock锁和synchronized 来说。都是互斥锁,读读也存在互斥。对此,我们需要读写锁。

如实例:

生产者和消费者而言

当一个线程负责生产,2个线程负责消费,生产者没有进行生产时,两个消费线程都可以去消费数据(这里我们不考虑 重复数据问题)

两个线程彼此还要争抢资源

privatestaticfinalint LINED_SIZE=1000;privatestaticint num=0;privatestaticfinalObject lock=newObject();privatestaticfinalLinkedList<Integer> linkedList=newLinkedList<>();publicstaticvoidmain(String[] args)throwsInterruptedException{             t1.start();         t2.start();         t3.start();          t1.setPriority(5);         t2.setPriority(5);         t3.setPriority(5);          t1.join();         t2.join();         t3.join();TimeUnit.SECONDS.sleep(2);System.out.println(" main  end ");}staticclassConsumerObjeimplementsRunnable{@Overridepublicvoidrun(){while(true){synchronized(lock){while(linkedList.size()==0){try{                            lock.wait();}catch(InterruptedException e){                            e.printStackTrace();}}try{Thread.sleep(5_00);}catch(InterruptedException e){                        e.printStackTrace();}System.out.println(Thread.currentThread().getName()+" : "+ linkedList.removeFirst());                    lock.notifyAll();}}}}staticclassProductObjeimplementsRunnable{@Overridepublicvoidrun(){while(true){synchronized(lock){while(linkedList.size()>= LINED_SIZE){try{                             lock.wait();}catch(InterruptedException e){                             e.printStackTrace();}}int n= num++;System.out.println(" 正在生产: "+ n);                     linkedList.addLast(n);                     lock.notifyAll();}}}}

思考

  • 为什么需要使用读写锁ReentrantReadWriteLock

  • 这个锁有什么好处?缺点?

好处: 读读不能互斥,提升锁性能,减少线程竞争。
缺点是:当读锁过多时候,写锁少,存在锁饥饿现象。

读写锁ReentrantReadWriteLock用法详解

ReentrantReadWriteLock 也提供了公平和非公平锁

基于构造默认非公平锁, ReentrantReadWriteLock 读写锁内部也是基于AQS队列实现的。

publicReentrantReadWriteLock(){this(false);}
//读写锁privatestaticReentrantReadWriteLock readWriteLock=newReentrantReadWriteLock(true);//写锁privatefinalstaticLock writeLock= readWriteLock.writeLock();//读锁privatefinalstaticLock readLock= readWriteLock.readLock();privatefinalstaticList<Long> longs=newArrayList<>();publicfinalstaticvoidmain(String[] args)throwsInterruptedException{//        new Thread(ReentrantReadWriteLockTest::write).start();//        TimeUnit.SECONDS.sleep(1);//        new Thread(ReentrantReadWriteLockTest::write).start();newThread(ReentrantReadWriteLockTest::write).start();TimeUnit.SECONDS.sleep(1);newThread(ReentrantReadWriteLockTest::read).start();newThread(ReentrantReadWriteLockTest::read).start();}staticvoidwrite(){try{             writeLock.lock();TimeUnit.SECONDS.sleep(1);System.out.println(Thread.currentThread().getName()+" write ");             longs.add(System.currentTimeMillis());}catch(InterruptedException e){             e.printStackTrace();}finally{             writeLock.unlock();}}staticvoidread(){try{             readLock.lock();TimeUnit.SECONDS.sleep(1);             longs.forEach(x->System.out.println(x));}catch(InterruptedException e){             e.printStackTrace();}finally{             readLock.lock();}}

可以看到我们写了一条数据,两条数据同时打印出来,读读是不互斥的。

Thread-0 write
1648997092090
1648997092090

读写锁 存在一个问题:
当读锁比例很多,写锁很少,锁竞争情况下,写锁抢到锁的机会就回少,读锁数量太大的情况下,写锁不一定能抢到锁.

我们使用非公平锁,来测试,启动5个读锁,一个写锁。

//读写锁privatestaticReentrantReadWriteLock readWriteLock=newReentrantReadWriteLock(false);//写锁privatefinalstaticLock writeLock= readWriteLock.writeLock();//读锁privatefinalstaticLock readLock= readWriteLock.readLock();privatefinalstaticList<Long> longs=newArrayList<>();publicfinalstaticvoidmain(String[] args)throwsInterruptedException{newThread(ReentrantReadWriteLockTest2::write).start();TimeUnit.SECONDS.sleep(1);//new Thread(ReentrantReadWriteLockTest2::read).start();//new Thread(ReentrantReadWriteLockTest2::read).start();for(int i=0; i<5; i++){newThread(ReentrantReadWriteLockTest2::read).start();}}staticvoidwrite(){for(;;){try{                writeLock.lock();TimeUnit.SECONDS.sleep(1);System.out.println(Thread.currentThread().getName()+" write ");                longs.add(System.currentTimeMillis());}catch(InterruptedException e){                e.printStackTrace();}finally{                writeLock.unlock();}}}staticvoidread(){for(;;){try{                   readLock.lock();TimeUnit.SECONDS.sleep(1);                   longs.forEach(x->System.out.println(x));}catch(InterruptedException e){                   e.printStackTrace();}finally{                   readLock.lock();}}}

测试结果这里就不写了,刚开始一直写,后来一直读,写锁机会很少,当读线程比例再大时,写的机会就更少了。

最后

ReentrantReadWriteLock 读写锁既有有点也有缺点

好处: 读读不能互斥,提升锁性能,减少线程竞争。
缺点是:当读锁过多时候,写锁少,存在锁饥饿现象。

使用时候需要控制读写比例,防止出现锁饥饿现象。

当出现读比例特别大时候,ReentrantReadWriteLock锁就不适合了,此时JDK8之后提供的StampedLock锁更适合读写比例大的场景