聊聊高并发(二十九)解析java.util.concurrent各个组件(十一) 再看看ReentrantReadWriteLock可重入读-写锁
上一篇聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁 讲了可重入读写锁的基本情况和基本的方法,显示了怎样实现的锁降级。可是以下几个问题没说清楚,这篇补充一下
1. 释放锁时的优先级问题。是让写锁先获得还是先让读锁先获得
2. 是否同意读线程插队
3. 是否同意写线程插队,由于读写锁一般用在大量读,少量写的情况,假设写线程没有优先级,那么可能造成写线程的饥饿
关于释放锁后是让写锁先获得还是让读锁先获得,这里有两种情况
1. 释放锁后,请求获取写锁的线程不在AQS队列
2. 释放锁后。请求获取写锁的线程已经AQS队列
假设是第一种情况。那么非公平锁的实现下,获取写锁的线程直接尝试竞争锁也不用管AQS里面先来的线程。获取读锁的线程仅仅推断是否已经有线程获得写锁(既Head节点是独占模式的节点),假设没有,那么就不用管AQS里面先来的准备获取读锁的线程。
static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() {
            return false; // writers can always barge
        }
        final boolean readerShouldBlock() {
            return apparentlyFirstQueuedIsExclusive();
        }
    }
在公平锁的情况下。获取读锁和写锁的线程都推断是否已经或先来的线程再等待了。假设有,就进入AQS队列等待。
static final class FairSync extends Sync {
        private static final long serialVersionUID = -2274990926593161451L;
        final boolean writerShouldBlock() {
            return hasQueuedPredecessors();
        }
        final boolean readerShouldBlock() {
            return hasQueuedPredecessors();
        }
    }
对于另外一种情况,假设准备获取写锁的线程在AQS队列里面等待。那么实际是遵循先来先服务的公平性的,由于AQS的队列是FIFO的队列。所以获取锁的线程的顺序是跟它在AQS同步队列里的位置有关系。
以下这张图模拟了AQS队列中等待的线程节点的情况
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvSVRlcl9aQw==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="">
1. Head节点始终是当前获得了锁的线程
2. 非Head节点在竞争锁失败后。acquire方法会不断地轮询。于自旋不同的是,AQS轮询过程中的线程是堵塞等待。
所以要理解AQS的release释放动作并非让兴许节点直接获取锁。而是唤醒兴许节点unparkSuccessor()。
真正获取锁的地方还是在acquire方法,被release唤醒的线程继续轮询状态,假设它的前驱是head,而且tryAcquire获取资源成功了,那么它就获得锁
public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
3. 图中Head之后有3个准备获取读锁的线程,最后是1个准备获取写锁的线程。
那么假设是AQS队列中的节点获取锁
情况是第一个读锁节点先获得锁。它获取锁的时候就会尝试释放共享模式下的一个读锁。假设释放成功了,下一个读锁节点就也会被unparkSuccessor唤醒,然后也会获得锁。
假设释放失败了。那就把它的状态标记了PROPAGATE,当它释放的时候。会再次取尝试唤醒下一个读锁节点
假设后继节点是写锁。那么就不唤醒
private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
 private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
       
        if (propagate > 0 || h == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
    }
private void doReleaseShared() {
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }
AQS的FIFO队列保证了在大量读锁和少量写锁的情况下,写锁也不会饥饿。
关于读锁能不能插队的问题,非公平性的Sync提供了插队的可能,可是前提是它在tryAcquire就成功获得了。假设tryAcquire失败了,它就得进入AQS队列排队。也不会出现让写锁饥饿的情况。
关于写锁能不能插队的情况,也是和读锁一样,非公平的Sync提供了插队的可能,假设tryAcquire获取失败,就得进入AQS等待。
最后说说为什么Semaphore和ReentrantLock在tryAcquireXX方法就实现了非公平性和公平性,而ReentrantReadWriteLock却要抽象出readerShouldBlock和writerShouldBlock的方法来单独处理公平性。
abstract boolean readerShouldBlock();
abstract boolean writerShouldBlock();
原因是Semaphore仅仅支持共享模式,所以它仅仅须要在NonfairSync和FairSync里面实现tryAcquireShared方法就能实现公平性和非公平性。
ReentrantLock仅仅支持独占模式,所以它仅仅须要在NonfairSync和FairSync里面实现tryAcquire方法就能实现公平性和非公平性。
而ReentrantReadWriteLock即要支持共享和独占模式。又要支持公平性和非公平性。所以它在基类的Sync里面用tryAcquire和tryAcquireShared方法来区分独占和共享模式。
在NonfairSync和FairSync的readerShouldBlock和writerShouldBlock里面实现非公平性和公平性。
聊聊高并发(二十九)解析java.util.concurrent各个组件(十一) 再看看ReentrantReadWriteLock可重入读-写锁的更多相关文章
- 聊聊高并发(二十)解析java.util.concurrent各个组件(二) 12个原子变量相关类
		
这篇说说java.util.concurrent.atomic包里的类,总共12个.网上有非常多文章解析这几个类.这里挑些重点说说. watermark/2/text/aHR0cDovL2Jsb2cu ...
 - 谈论高并发(三十)解析java.util.concurrent各种组件(十二) 认识CyclicBarrier栅栏
		
这次谈话CyclicBarrier栅栏,如可以从它的名字可以看出,它是可重复使用. 它的功能和CountDownLatch类别似,也让一组线程等待,然后开始往下跑起来.但也有在两者之间有一些差别 1. ...
 - 聊聊高并发(四十)解析java.util.concurrent各个组件(十六) ThreadPoolExecutor源代码分析
		
ThreadPoolExecutor是Executor运行框架最重要的一个实现类.提供了线程池管理和任务管理是两个最主要的能力.这篇通过分析ThreadPoolExecutor的源代码来看看怎样设计和 ...
 - 聊聊高并发(二十五)解析java.util.concurrent各个组件(七) 理解Semaphore
		
前几篇分析了一下AQS的原理和实现.这篇拿Semaphore信号量做样例看看AQS实际是怎样使用的. Semaphore表示了一种能够同一时候有多个线程进入临界区的同步器,它维护了一个状态表示可用的票 ...
 - 聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁
		
这篇讲讲ReentrantReadWriteLock可重入读写锁,它不仅是读写锁的实现,而且支持可重入性. 聊聊高并发(十五)实现一个简单的读-写锁(共享-排他锁) 这篇讲了怎样模拟一个读写锁. 可重 ...
 - 018-并发编程-java.util.concurrent.locks之-ReentrantReadWriteLock可重入读写锁
		
一.概述 ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个读线程同时访问,但不允许写线程和读线程.写线程和写线程同时访问.相对 ...
 - 聊聊高并发(三十九)解析java.util.concurrent各个组件(十五) 理解ExecutorService接口的设计
		
上一篇讲了Executor接口的设计,目的是将任务的运行和任务的提交解耦.能够隐藏任务的运行策略.这篇说说ExecutorService接口.它扩展了Executor接口,对Executor的生命周期 ...
 - 聊聊高并发(二十四)解析java.util.concurrent各个组件(六) 深入理解AQS(四)
		
近期总体过了下AQS的结构.也在网上看了一些讲AQS的文章,大部分的文章都是泛泛而谈.又一次看了下AQS的代码,把一些新的要点拿出来说一说. AQS是一个管程.提供了一个主要的同步器的能力,包括了一个 ...
 - 聊聊高并发(四十四)解析java.util.concurrent各个组件(二十) Executors工厂类
		
Executor框架为了更方便使用,提供了Executors这个工厂类.通过一系列的静态工厂方法.能够高速地创建对应的Executor实例. 仅仅有一个nThreads參数的newFixedThrea ...
 
随机推荐
- JavaScript快速入门(六)——DOM
			
概念扫盲 DOM DOM是 Document Object Model(文档对象模型)的缩写,是W3C(万维网联盟)的标准.DOM 定义了访问 HTML 和 XML 文档的标准:“W3C 文档对象模型 ...
 - Android 驱动(二) IIC简单介绍
			
一. I2C简单介绍 I2C(Inter-Integrated Circuit)总线是一种由 Philips 公司开发的两线式串行总线,用于连接微控制器及其外围设备.I2C 总线最基本的长处就是简单性 ...
 - Struts2通过自己定义拦截器实现登录之后跳转到原页面
			
这个功能对用户体验来说是非常重要的.实现起来事实上非常easy. 拦截器的代码例如以下: package go.derek.advice; import go.derek.entity.User; i ...
 - ConnectivityManager
			
ConnectivityManager 主要管理网络连接的相关的类它主要负责的是1 监视网络连接状态 包括(Wi-Fi, GPRS, UMTS, etc)2 当网络状态改变时发送广播通知3 当网络连接 ...
 - Git 图解剖析(转)
			
git中文件内容并没有真正存储在索引(.git/index)或者提交对象中,而是以blob的形式分别存储在数据库中(.git/objects),并用SHA-1值来校验. 索引文件用识别码列出相关的bl ...
 - dll导入导出资源文件查看工具 InspectExe
			
InspectExe lets you explore and diagnose problems with Win32 applications. It is integrated directly ...
 - 虚拟机centOS中安装Redis,主机Redis Destop Manager不能访问虚拟机Redis server的解决方案
			
今天在学些redis的时候碰到个问题,发现主机Redis Destop Manager不能访问虚拟机Redis server的解决方案,找了一些网上的资料,原因可能有两个,整理记录下来: 1. Red ...
 - rzsz不能大于4G,securefx传5.2G没有问题,
			
rzsz不能大于4G,securefx传5.2G没有问题, 查看系统限制: $ulimit -acore file size (blocks, -c) 0data seg size ...
 - C++ 指针—02 指针与引用的对照
			
★同样点: ●都是地址的概念: 指针指向一块内存,它的内容是所指内存的地址:而引用则是某块内存的别名. ★不同点: ●指针是一个实体,而引用仅是个别名: ●引用仅仅能在定义时被初始化一次,之后不可变: ...
 - android设置中的Preferencescreen使用方法介绍与分析
			
今天主要研究了一下设置中的Preferencescreen应用,它不仅可以作为设置界面显示,并且还可以启动activity,以下主要是对启动activity的介绍 1. Preferencescree ...