美文网首页
Java并发——读写锁

Java并发——读写锁

作者: Q南南南Q | 来源:发表于2017-08-01 15:21 被阅读0次

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

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

    ReentrantReadWriteLock的特性

    读写锁的实现分析

    1 读写状态的设计

    读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。

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

    读写锁状态的划分方式

    读写锁是如何迅速确定读和写各自的状态呢?答案是通过位运算。假设当前同步状态值为S,写状态等于S&0x0000FFFF(将高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)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态
    • 如果当前线程已经获取了读锁,则增加读状态。
    • 如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。
    protected final int 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;
        }
    }
    

    4 锁降级

    如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。

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

    那么锁降级的设计的目的是什么呢?为何要在拥有写锁的前提下去获取读锁?
    通过查看一些文章,写一下自己的理解:

    锁降级的目的其实是为了让线程对数据变化敏感,如果先释放写锁,再获取读锁,可能在获取之前,会有其他线程获取到写锁,阻塞读锁的获取,就无法感知数据变化了。所以需要先hold住写锁,保证数据无变化,获取读锁,然后再释放写锁。

    例如有多个线程对同一块数据区域data进行读写操作,要求对每次数据的更改敏感。假设t1时刻data区域被写线程将状态s0更改为s1,更改完后若直接释放锁,那么可能会有其他线程获取写锁,将data区域的状态从s1更改为s2,这样一来整个过程就无法感知到data区域的s1状态。

    如果采用了锁降级,那么获取写锁的线程t将data区域状态更改为s1后便持有读锁,那么其它想获取写锁的线程将会阻塞,直到线程t将读锁释放,那么这个过程中将会感知到data区域的s1状态。

    锁降级对比

    相关文章

      网友评论

          本文标题:Java并发——读写锁

          本文链接:https://www.haomeiwen.com/subject/sjrjlxtx.html