Java并发——读写锁

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

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

Java并发——读写锁_第1张图片
ReentrantReadWriteLock的特性

读写锁的实现分析

1 读写状态的设计

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

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

Java并发——读写锁_第2张图片
读写锁状态的划分方式

读写锁是如何迅速确定读和写各自的状态呢?答案是通过位运算。假设当前同步状态值为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并发——读写锁_第3张图片
锁降级对比

你可能感兴趣的:(Java并发——读写锁)