当前位置: 首页 > 科技观察

有了CopyOnWrite,为什么还需要ReadWriteLock?

时间:2023-03-18 23:09:17 科技观察

介绍前面我们介绍了《看了CopyOnWriteArrayList后自己实现了一个CopyOnWriteHashMap》关于CopyOnWrite容器,但是它也有一些缺点:内存使用问题:因为CopyOnWrite的copy-on-write机制,每次写操作都会有两个内存数组对象,如果数组对象占用内存大,频繁写入会导致频繁的YongGC和FullGC数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。读操作的线程可能不会立即读取新修改的数据,因为修改操作发生在副本上。但最终修改操作会完成并更新容器,因此这就是最终一致性。当时说我们可以用Collections.synchronizedList()来解决这两个缺点。找一个无非就是在列表的增删改查方法中加入synchronized实现。我们知道synchronized其实就是排他锁(exclusivelock)。如果你不知道排他锁是什么,可以看这篇这篇文章,基本上介绍了java中所有的锁。但是这样的话,就会出现性能问题。如果读多写少,每次读都需要获取锁,读完再释放锁。这将导致每个读取请求都获取锁。但是读不会造成数据不安全,会造成性能瓶颈。为了解决这个问题,出现了一种新的锁,ReadWriteLock。什么是读写锁根据名字我们也可以大致猜到,就是有两种锁,分别是读锁和写锁。一个读锁可以让多个读线程同时获取,但是当一个写线程访问它时,所有的读线程和其他写线程都会被阻塞。同一时刻只有一个写线程能够成功获取到写锁,其他的都会被阻塞。读写锁实际上维护了两把锁,读锁和写锁,以读锁和写锁来区分。在读多写少的情况下,并发性相比排他锁有很大的提升。java中读写锁的实现是ReentrantReadWriteLock,它有以下特点:公平选择:支持非公平(默认)和公平两种锁获取方式,吞吐量还是不公平大于公平;Reentrant:支持可重入Input,读锁获取后可以再次获取,写锁获取后可以再次获取写锁,也可以同时获取读锁;锁降级:按照获取写锁,获取读锁,释放写锁的顺序,写锁可以降级为读锁的使用ReentrantReadWriteLock以官网https://docs.oracle.com/为例javase/8/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html看看它是如何使用的privatefinalLockr=rwl.readLock();privatefinalLockw=rwl.writeLock();publicDataget(Stringkey){r.lock();try{returnm.get(key);}finally{r.unlock();}}publicString[]allKeys(){r.lock();try{returnm.keySet().toArray();}finally{r.unlock();}}publicDataput(Stringkey,Datavalue){w.lock();try{returnm.put(key,value);}finally{w.unlock();}}publicvoidclear(){w.lock();try{m.clear();}finally{w.unlock();}}}这是使用起来非常简单明了。和ReentrantLock的用法基本一样。它在写时获取写锁,写完后释放写锁。,读的时候获取读锁,读完释放读写。读写锁的实现分析我们知道,ReentrantLock是通过状态来控制锁的状态的,前面介绍的《Java高并发编程基础三大利器之Semaphore》《Java高并发编程基础三大利器之CountDownLatch》《Java高并发编程基础三大利器之CyclicBarrier》就是通过状态来实现的。ReentrantReadWriteLock无疑是通过AQS的state来实现的,但是state是一个int值怎么来读锁和写锁。读写锁状态的实现分析如果看过线程池的源码就知道,线程池的状态和线程数是由一个int类型的原子变量控制的(高3位保存运行状态,低29位保存线程数)的。同样的ReentrantReadWriteLock也是通过一个状态的高16位和低16位分别控制读状态和写状态。我们来看看它是如何通过一个字段实现读写分离的,staticfinalintSHARED_SHIFT=16;staticfinalintSHARED_UNIT=(1<>>SHARED_SHIFT;}/**Returnsthenumberofexclusiveholdsrepresentedincount*/staticintexclusiveCount(intc){returns&EXCLUSIVE_MASK;}符号右移16位,即高16位采取同步状态。exclusiveCount:写锁的个数,我们要看静态变量EXCLUSIVE_MASK:它是1左移16位再减1,即0X0000FFFF(1<MAX_COUNT)thrownewError("Maximumlockcountexceeded");//重入获取ac获取写锁setState(c+acquires);returntrue;}//writerShouldBlock公平锁和非公平锁判断if(writerShouldBlock()||!compareAndSetState(c,c+acquires))returnnfalse;setExclusiveOwnerThread(current);returntrue;}写完锁,接下来一定是读锁。既然读锁是共享锁,那么tryAcquireShared也要重写。这样就不贴代码了,跟读锁差不多。分析了一下,其实理解AQS再基于AQS来看这些东西还是比较容易的。读写锁的升级和降级前面我们提到读写锁可以降级,但是并没有说是否可以升级。我们先来看看什么是锁降级和锁升级。锁降级:从写锁到读锁;它的流程是先持有写锁,获取读锁,然后释放写锁。如果持有写锁,释放写锁,再获取读锁,这不是锁降级。为什么要锁定降级?主要是为了保证数据的可见性。如果当前线程没有获取读锁而是直接释放了写锁,假设此时另一个线程(记为线程T)获取了写锁并修改了数据,则当前线程无法感知到数据的更新线程T,如果当前线程获得了读锁,即按照锁降级的步骤,线程T会被阻塞,直到当前线程使用数据释放读锁,线程T才能获得数据的写锁更新。源于《Java 并发编程的艺术》锁升级:从读锁到写锁。先持有读锁,再获取写锁(这样不会成功),因为获取写锁是独占锁。如果读锁被占用,写锁会被放入队列,等到读锁全部释放后,才有可能获得写锁。思考题本文主要介绍单机情况下的读写锁,如果想实现分布式读,写锁怎么实现?ReentrantReadWriteLock的饥饿问题怎么解决?写锁变得更难了,因为可能一直有读锁,写锁得不到。)最后,由于本人见识不足,难免有错误,如有发现错误,请留言指出,来,我改正。可以通过以下二维码关注,请关注联系爪哇财经公众号转载本文。