`
沉沦的快乐
  • 浏览: 55806 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

并发编程之读写锁ReentrantReadWriteLock实现

阅读更多

引子   

我们团队有维护这样一个类似比价的系统。其中有一个A方法需要查询好几个第三方的http接口,以获取某些信息进行汇总分析。虽然是通过多线程并发的去访问第三方接口,但是有些第三方系统不稳定,只要其中一个挂了就会影响整个方法的效率。团队成员经常在接到报警信息之后手动把不稳定的接口下线,这给大家的正常生活带来了麻烦,因为你有时候不得不不周末或者半夜起来操作。于是我做了一个自动下线功能,假如某个接口在1分钟内抛出的异常大于某个阈值之后自动下线一段时间,并在下线一段时间之后再自动上线,如果上线之后发现异常还没有减少则继续下线。为了实现这个功能,首先需要在抛异常的采集信息,并判断是否需要下线。A方法在请求第三方接口之前需要判断这个接口有没有下线,有下线之后则不再调用该http请求。在这种场景下,明显是读的行为比写的行为多,因为每次A方法都要读操作,而只有在抛异常的情况下才需要写操作。为了尽量保证方法的性能,想到了用读写锁来实现,但是之前没有用过读写锁,于是对读写锁的实现进行了学习。

读写锁

   读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。读写锁允许多个读者访问共享资源;读者和写者,多个写着只允许一个访问共享资源,因此在读操作多而写操作少的场景下,读写锁比普通的互斥锁能获得更高的并发能力。

 

读写锁规则

1.获得读锁:没有其他线程获得写锁,并且没有线程在请求写操作。

2.获得写锁:没有线程获得读锁和写锁。

3.可重入:如果一个线程获得读/写锁,那么该线程可以再次获得该锁。

4.读锁升级为写锁:有时候我们希望一个获得读锁的线程,也能获得写锁。读锁升级为写锁必须满足该线程是唯一拥有读锁的条件,即除了该线程之外,再没有其他线程拥有读锁。

5.写锁降级为读锁:即拥有写锁的线程,可以同时获得读锁。因为一个线程拥有了写锁,那么就不会有其他线程获得写锁和读锁了,而对于一个线程同时拥有读锁和写锁是没有什么危险的。

6.线程活跃度风险:如果读操作非常频繁,那么写操作可能一直获取不到写锁,从而产生写线程”饥饿“,那么需要一直机制去解决活跃度风险。

 

读写锁使用

java的读写锁是用ReentrantReadWriteLock实现的,ReentrantReadWriteLock使用比较简单,首先创建读写锁ReentrantReadWriteLock的实例,读方法用读锁锁住,写方法用写锁锁住,与所有显式锁一样,必须在finally中释放锁,下面是使用方法示例:

 

public ReadWriteLockExample {

       private final ReadWriteLock lock = new ReentrantReadWriteLock();
	private final Lock readLock = lock.readLock();
	private final Lock writeLock = lock.writeLock();


     public Object readMothed() {
          readLock.lock();
	     try {
		 your code;
	     } finally {
		readLock.unlock();
	    }
         }

    public void writeMothed() {
        writeLock.lock();
        try{
	    your code;
        } finally {
	    writeLock.unlock();
        }
    }
}
 

 

读写锁原理及实现

前面说过,java的读写锁是ReentrantReadWriteLock类实现的。他有3个自己实现的内部类ReadLock, writerLock,Sync。 同时Sync又有2个子类,一个是FairSync和NonFairSync,主要区别是获得读写锁的公平性,即解决线程饥饿的方法不同。

 

/** Inner class providing readlock */
    private final ReentrantReadWriteLock.ReadLock readerLock;
    /** Inner class providing writelock */
    private final ReentrantReadWriteLock.WriteLock writerLock;
    /** Performs all synchronization mechanics */
    private final Sync sync;
 

 

 

 顾名思义,ReadLock是读锁,writerLock是写锁,Sync是真正实现读写锁功能的类。ReadLock,writerLock的Lock,unLock方法都是委托Sync类来实现的。Sync扩展自抽象类AbstractQueuedSynchronizer,关于AQS的介绍,可以参考并发编程之AbstractQueuedSynchronizer原理剖析。使用一个int型state字段来管理读写请求线程数。高16位表示持有读锁的线程数,低16位表示持有写锁的线程数(0或1)以及请求写锁的请求数。使用threadlocal readHolds来记录当前线程持有的读锁数,同时还用了一个cachedHoldCounter来保存上一次成功获得读锁的线程及其读锁数量,还有一个exclusiveOwnerThread字段来保存拥有写锁的线程。最后Sync还继承出fairSync与nonfairSync类来解决线程活跃度问题。

读锁的实现

    读锁的lock是委托tryAcquireShared方法来实现的,如果tryAcquireShared返回小于0则通过自旋的方式继续调用tryAcquireShared方法直到获得锁或被中断为止。下面是tryAcquireShared的具体实现:

protected final int tryAcquireShared(int unused) {
            Thread current = Thread.currentThread();
            int c = getState();
            if (exclusiveCount(c) != 0 &&
                getExclusiveOwnerThread() != current)
                return -1;
            if (sharedCount(c) == MAX_COUNT)
                throw new Error("Maximum lock count exceeded");
            if (!readerShouldBlock(current) &&
                compareAndSetState(c, c + SHARED_UNIT)) {
                HoldCounter rh = cachedHoldCounter;
                if (rh == null || rh.tid != current.getId())
                    cachedHoldCounter = rh = readHolds.get();
                rh.count++;
                return 1;
            }
            return fullTryAcquireShared(current);
        }
 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
                return -1;  表示如果请求和持有写锁的线程数不为0并且持有写锁的线程不是当前线程 则不能获得读锁。言外之意就是,如果没有线程持有写锁也没有其他线程请求写锁时,有机会获得读锁,同时如果持有写锁的线程是当前线程,那么当前线程也有机会获得读锁。这实现了上面读写锁的第1,5条规则。

 由于state的低16位表示持有读锁的线程数,如果该数超过了0xFFFF抛出 Error("Maximum lock count exceeded");错误。:

if (sharedCount(c) == MAX_COUNT)
                throw new Error("Maximum lock count exceeded");

 如果前面两个条件都没有拦截住线程获取读锁的决心,那么最后一关来了:

 

 if (!readerShouldBlock(current) &&
                compareAndSetState(c, c + SHARED_UNIT))
 readerShouldBlock(current)方法判断当前线程需不需要继续阻塞,这个条件是为解决写锁活跃度风险。readerShouldBlock方法由Sync的子类fairSync和nonfairSync来实现。在nonfairSync中,如果阻塞队列中下一个请求是写请求,那么当前线程就不能获得读锁了。在fairSync中,如果阻塞队列为空或者当前线程是队列的head时,才允许获当前线程获得读锁。最后同时满足CAS条件时,这个线程才终于获得读锁了。

 

如果前面3个if条件都没能返回-1或1时,最终会调用fullTryAcquireShared方法再判断一遍完整的获取读锁逻辑,由于跟前面tryAcquireShared差别不大,只是增加了一个自旋直到能返回结果。

 

 final int fullTryAcquireShared(Thread current) {
            /*
             * This code is in part redundant with that in
             * tryAcquireShared but is simpler overall by not
             * complicating tryAcquireShared with interactions between
             * retries and lazily reading hold counts.
             */
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != current.getId())
                rh = readHolds.get();
            for (;;) {
                int c = getState();
                int w = exclusiveCount(c);
                if ((w != 0 && getExclusiveOwnerThread() != current) ||
                    ((rh.count | w) == 0 && readerShouldBlock(current)))
                    return -1;
                if (sharedCount(c) == MAX_COUNT)
                    throw new Error("Maximum lock count exceeded");
                if (compareAndSetState(c, c + SHARED_UNIT)) {
                    cachedHoldCounter = rh; // cache for release
                    rh.count++;
                    return 1;
                }
            }
        }
 

 

 

写锁的实现

写锁的lock是委托Sync的tryAcquire方法来实现的,但是由于fairSync和nonfairSync对了公平性策略不同而采用可不同的实现方法,所以这里分开来写。

nonfairSync的写锁调用的是nonfairTryAcquire方法

 

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    首先,c==0表示当前既没有线程获得读锁,也没有线程获得写锁,那么只要当前线程对state的CAS操作成功,则就可以获得写锁,并设置当前线程为获得写锁的线程;如果有其他线程获得读锁或写锁时,如果获得写锁的线程是当前线程,那么可以继续获得写锁。这实现了前面读写锁规则的第2条和第3条。

 

fairSync的写锁实现

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (isFirst(current) &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }

    该方法其实与nonfairSync的整体逻辑差不多,只是增加了一个是否阻塞的的判断isFirst(current)。当前既没有线程获得读锁,也没有线程获得写锁,那么当前线程有没有机会获得写锁,还得看阻塞队列的情况,如果阻塞队列为空,或者当前线程是阻塞队列的head,那么就有获得写锁的机会。

 

 

 

分享到:
评论

相关推荐

    汪文君高并发编程实战视频资源下载.txt

    │ 高并发编程第一阶段30讲、如何实现一个自己的显式锁Lock精讲下(让锁具备超时功能).mp4 │ 高并发编程第一阶段31讲、如何给你的应用程序注入钩子程序,Linux下演示.mp4 │ 高并发编程第一阶段32讲、如何捕获...

    汪文君高并发编程实战视频资源全集

    │ 高并发编程第一阶段30讲、如何实现一个自己的显式锁Lock精讲下(让锁具备超时功能).mp4 │ 高并发编程第一阶段31讲、如何给你的应用程序注入钩子程序,Linux下演示.mp4 │ 高并发编程第一阶段32讲、如何捕获...

    Java并发编程原理与实战

    读写锁认识与原理.mp4 细读ReentrantReadWriteLock源码.mp4 ReentrantReadWriteLock锁降级详解.mp4 线程安全性问题简单总结.mp4 线程之间的通信之wait notify.mp4 通过生产者消费者模型理解等待唤醒机制.mp4 ...

    Java 并发编程原理与实战视频

    java并发编程原理实战 第2节理解多线程与并发的之间的联系与区别 [免费观看] 00:11:59分钟 | 第3节解析多线程与多进程的联系以及上下文切换所导致资源浪费问题 [免费观看] 00:13:03分钟 | 第4节学习并发的四个...

    【2018最新最详细】并发多线程教程

    11.深入理解读写锁ReentrantReadWriteLock 12.详解Condition的await和signal等待通知机制 13.LockSupport工具 14.并发容器之ConcurrentHashMap(JDK 1.8版本) 15.并发容器之ConcurrentLinkedQueue 16.并发容器之...

    龙果 java并发编程原理实战

    龙果 java并发编程原理实战 第2节理解多线程与并发的之间的联系与区别 [免费观看] 00:11:59分钟 | 第3节解析多线程与多进程的联系以及上下文切换所导致资源浪费问题 [免费观看] 00:13:03分钟 | 第4节学习并发的四...

    龙果java并发编程完整视频

    第25节读写锁认识与原理00:18:04分钟 | 第26节细读ReentrantReadWriteLock源码00:30:38分钟 | 第27节ReentrantReadWriteLock锁降级详解00:13:32分钟 | 第28节线程安全性问题简单总结00:15:34分钟 | 第29节线程...

    java并发编程

    第25节读写锁认识与原理00:18:04分钟 | 第26节细读ReentrantReadWriteLock源码00:30:38分钟 | 第27节ReentrantReadWriteLock锁降级详解00:13:32分钟 | 第28节线程安全性问题简单总结00:15:34分钟 | 第29节线程...

    举例说明Java多线程编程中读写锁的使用

    以下示例为 java api并发库中 ReentrantReadWriteLock自带的实例,下面进行解读 class CachedData { Object data; volatile boolean cacheValid; ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();...

    java核心知识点整理.pdf

    1. 目录 1. 2. 目录 .........................................................................................................................................................1 JVM .........................

    JAVA核心知识点整理(有效)

    1. 目录 1. 2. 目录 .........................................................................................................................................................1 JVM ........................

Global site tag (gtag.js) - Google Analytics