java多线程解说【捌】_锁实现:读写锁ReentrantReadWriterLock


前面的文章介绍了几种锁的实现:


java多线程解说【肆】_锁实现:wait()/notify()

java多线程解说【伍】_锁实现:ReentrantLock的实现

java多线程解说【陆】_锁实现:Condition的实现



这几种锁都是排他锁,也就是说同一时刻只允许一个线程进行访问,而对其他线程的不论什么操作都会阻塞,这样当在读写场景下性能和并发性是不太友好的。那么如果我们希望在读写场景下同一时刻可以允许多个读操作,然后读锁和写锁进行分离处理应该怎么办呢,这就要说说今天要介绍了的读写锁(ReentrantReadWriterLock)了。


ReentrantReadWriterLock简介


还是先回到我们juc包的结构图:




从图中可以得知,juc包中提供了一个ReadWriteLock接口,而它有一个默认实现类就是ReentrantReadWriterLock。故名思义,它也是可重入的。ReentrantReadWriterLock内部维护了一对锁,即读锁(ReadLock)和写锁(WriteLock),而这对锁正是实现了juc包中的Lock接口,而提供了进行锁操作的lock()和unlock()方法。


在JDK5之前没有读写锁的时候,如果想完成对对象的读写操作只能使用synchronized同步块来实现,而这种实现方式会只允许读或写的一个操作在进行,这样可能保证程序不会出现脏读,但是程序的并发性十分低下。而ReentrantReadWriterLock实现了读锁和写锁的分离:当获取读锁时,写操作会阻塞;获取写锁时,读写操作都会阻塞。这样实现的好处是,即可以保证数据的准确性,又可以让多个读操作同时进行处理。通过ReentrantReadWriterLock提供的锁操作接口,相比于其他所实现方式我们可以更简单明了地在我们代码中实现读写锁的操作。


ReentrantReadWriterLock共有如下3个特性:

1.公平性(同RenntrantLock的公平性一致);

2.重入性(一个线程可以多次获取读锁和写锁);

3.锁降级(写锁可以降级为读锁,但是不支持升级);


由于ReentrantReadWriterLock内部实现比较复杂,功能实现的同时伴有一定的性能消耗。通常建议在如下场景时可以使用:

1.读操作比写操作频繁的多;

2.读线程持有锁时间比较长;



ReentrantReadWriterLock的API介绍


isFair()

判断读写锁是否为公平锁


getOwner()

获取占有读写锁的写锁的线程对象


getReadLockCount()

返回当前读锁获取次数。读锁可以在一个线程内多次获取,也可以在多个线程内获取,这里返回的是总获取次数


isWriteLocked()

判断写锁是否被线程占有


isWriteLockedByCurrentThread()

判断写锁是否被当前线程占有


getWriteHoldCount()

获取当前线程持有的写锁获取次数(JDK6加入)。同读锁一样,写锁也是可重入的,区别在于写锁只能在同一个线程内重入。如果当前线程没有持有写锁,则返回0


getReadHoldCount()

返回当前线程获取读锁的次数(JDK6加入)。如果没有则返回0

getQueuedWriterThreads()

获取在排队获取写锁的线程集合。因为这个集合是随时变化的,所以返回的结果只是一个估值,本方法适用于监控系统状态


getQueuedReaderThreads()

获取在排队获取读锁的线程集合。因为这个集合是随时变化的,所以返回的结果只是一个估值,本方法适用于监控系统状态


hasQueuedThreads()

判断是否有线程在等待获取读锁或写锁,本方法适用于监控系统状态,原因同上


hasQueuedThread(Thread thread)

判断给定线程是否在等待获取读锁或写锁,本方法适用于监控系统状态,原因同上


getQueueLength()

判断在等待获取读锁或写锁的线程个数,本方法适用于监控系统状态,原因同上


getQueuedThreads()

判断在等待获取读锁或写锁的线程集合,本方法适用于监控系统状态,原因同上



ReentrantReadWriterLock的实现


ReentrantReadWriterLock的实现比较复杂,这里先简单几个功能的实现,后面有机会专门再总结一篇文章补充。


首先看看ReentrantReadWriterLock是如何记录读锁和写锁的重入次数的。因为读写锁的读锁和写锁的获取是有一定的限制在里面(比如写锁被占有时读锁就不能被获取),而这个限制就是依靠记录读锁和写锁的重入次数来实现的。所以获取这个读锁和写锁的重入次数的操作一定是原子操作。ReentrantReadWriterLock内部维护了一个Sync静态对象,该对象有一个SHARED_SHIFT静态整型变量。众所周知int值为32位,ReentrantReadWriterLock就是在这个整型变量上维护了两种状态,将变量切分成了2个部分,高16位表示读,低16位表示写。


写状态=SHARED_SHIFT & 0x0000FFFF(将高16位抹去);
读状态=SHARED_SHIFT>>>16(无符号状态向右移动16位);
写状态+1时,即SHARED_SHIFT+1;
读状态=1时,即SHARED_SHIFT+(1<<16);


ReentrantReadWriterLock由此实现了按位切割使用这个变量,从而实现了读锁和写锁的重入次数的记录。


至于读锁和写锁的获取和释放,都使用了CAS机制来保障线程安全,其实现和ReentrantLock的实现类似,但是有一些自己的判断逻辑在里面。此处我们后面有时间再分析。下面说说锁降级。



锁降级


什么是锁降级?锁降级是指一个线程当前拥有写锁,然后获取到读锁,随后把先前拥有的写锁释放的过程。


下面是锁降级的实现代码:


public class ProcessData {  
    private static final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();  
    private static final Lock readLock = rwl.readLock();  
    private static final Lock writeLock = rwl.writeLock();  
    private volatile boolean update = false;  
  
    public void processData() {  
        readLock.lock();  
        if (!update) {  
            readLock.unlock();  
            writeLock.lock();  
            try {  
                if (!update) {  
                    update = true;  
                }  
                readLock.lock();  
            } finally {  
                writeLock.unlock();  
            }  
        }  
        try {  
        } finally {  
            readLock.unlock();  
        }  
    }  
}


我们为什么需要锁降级?主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程获取了写锁并修改了数据,那么当前线程无法感知此次数据修改。如果当前线程获取读锁,即遵循锁降级的步骤,则此次数据修改将被阻塞,直到当前线程使用数据并释放读锁以后才能进行。


关于读写锁的介绍先到这里,下篇文章说说LockSupport工具类。


java多线程解说【玖】_锁实现:LockSupport工具类

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值