zoukankan      html  css  js  c++  java
  • 读写锁

    Mutex和ReentrantLock基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写现场均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

    除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。

    一般情况下,读写锁的性能都会比排他锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排他锁更好的并发性和吞吐量。Java并发包提供读写锁的实现时ReentrantReadWriteLock,它提供的特性如表所示:

    特性 说明
    公平性选择 支持非公平(默认)和公平的所获取方式,吞吐量还是非公平优于公平
    重进入 该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁
    锁降级 遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁

    读写锁的接口与示例

    ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()方法和writerLock()方法,而其实现——ReentrantReadWriteLock,除了接口方法之外,还提供了一些便于外界监控其内部工作状态的方法,这些方法以及描述如表:

    方法名称 描述
    int getReadLockCount() 返回当前读锁被获取的次数。该次数不等于获取读锁的线程数,例如,仅一个线程,它连续获取(重进入)了n次读锁,那么占据读锁的线程数是1,但该方法返回n
    int getReadHoldCount() 返回当前线程获取读锁的次数。该方法再Java 6中加入到ReentrantReadWriteLock中,使用ThreadLocal保存当前线程获取的次数,这也使得Java 6的实现变得更加复杂
    boolean isWriteLocked() 判断写锁是否被获取
    int getWriteHoldCount() 返回当前写锁获取的次数

    读写锁的实现分析

    1.读写状态的设计

    读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。

    如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写,划分方式如图:

    当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。读写锁是如何快速确定读和写各自的状态呢?答案是通过位运算。假设当前同步状态值位S,写状态等于S&0x0000FFFF(j将高16位全部抹去),读状态等于S>>>16(无符号补0右移16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是S+0x00010000。

    根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。


    2.写锁的获取与释放

    写锁是一个支持重进入的排他锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态,获取写锁的代码如下:

    protected final boolean tryAcquire(int acquires){
        Thread current = Thread.currentThread();
        int c = getState();
        int w = exclusiveCount(c);
        if(c != 0){
            //存在读锁或者当前获取线程不是已经获取写锁的线程
            if(w == 0 || current != getExclusiveOwnerThread()){
                return false;
            }
            if(w + exclusiveCount(acquires) > MAX_COUNT){
                throw new Error("Maximum lock count exceeded");
            }
            setState(c + acquires);
            return true;
        }
        if(writerShouldBlock() || !compareAndSetState(c,c + acquires)){
            return false;
        }
        setExclusiveOwnerThread(current);
        return true;
    }
    

    该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他线程都是放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。


    3.读锁的获取与释放

    读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写现场访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护,这使获取读锁的实现变得复杂。因此,这里将获取读锁的代码做了删减,保留了必要的部分,如代码:

    protected final itn tryAcquireShared(int unused){
        for(;;){
            int c = getState();
            int nextc = c + (1 << 16);
            if(nextc < c){
                throw new Error("Maximum lock count exceeded");
            }
            if(exclusiveCount(c) != 0&& owner != Thread.currentThread()){
                return -1;
            }
            if(compareAndSetState(c, nextc)){
                return 1;
            }
        }
    }
    

    在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。

    读锁的每次释放(线程安全的,可能有多个读线程同时释放锁)均减少读状态,减少的治时(1<<16)。


    4.锁降级

    锁降级指的是写锁降级成为读锁。

    锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

    接下来看一个锁降级的示例。

    public void processDate(){
        readLock.lock();
        if(!update){
            //必须先释放读锁
            readLock.unlock();
            //锁降级从写锁获取到开始
            writeLock.lock();
            try{
                if(!update){
                    //准备数据的流程(略)
                    update = true;
                }
                readLock.lock();
            }finally{
                writeLock.unlock();
            }
            //锁降级完成,写锁降级为读锁
        }
        try{
            //使用数据的流程(略)
        }finally{
            readLock.unlock();
        }
    }
    

    上面示例中,当数据发生变更后,update变量(布尔类型且volatile修饰)被设置为false,此时所有访问processData()方法的线程都能够感知到变化,但只有一个线程能够获取到写锁,其他线程会被阻塞在读锁和写锁的lock()方法上。当前线程获取写锁完成数据准备之后,再获取读锁,随后释放写锁,完成锁降级。

    锁降级中读诵的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前先不获取读锁而是直接释放写锁,假设此刻另一个线程(基座现场T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。

  • 相关阅读:
    Leetcode 238. Product of Array Except Self
    Leetcode 103. Binary Tree Zigzag Level Order Traversal
    Leetcode 290. Word Pattern
    Leetcode 205. Isomorphic Strings
    Leetcode 107. Binary Tree Level Order Traversal II
    Leetcode 102. Binary Tree Level Order Traversal
    三目运算符
    简单判断案例— 分支结构的应用
    用switch判断月份的练习
    java基本打印练习《我行我素购物系统》
  • 原文地址:https://www.cnblogs.com/Tu9oh0st/p/10162658.html
Copyright © 2011-2022 走看看