简介

之前我们主要介绍了ReentrantLock和其相关的Condition的内容。今天我们主要会过一下ReentrantReadWriteLock的代码。现实中,读写锁的使用非常多。如果你也是一个大数据工程师,同时使用过Spark的话,就会知道Spark内部有一个BlockInfoManager的类。在大数据计算中,常用的是基于复制的模式,读多写少的任务非常常见,有读写锁可以有效的减少锁竞争,提高数据查询速率。今天这一节,我们就简单介绍一下ReentrantReadWriteLock和它背后的一点数学知识。

读写锁的3条规定:

  1. 允许多个线程同时读共享变量
  2. 只允许一个线程写共享变量
  3. 如果写线程正在执行写出操作,那么禁止其他线程读写共享变量。

ReentrantReadWriteLock的特殊属性:锁降级:遵循先获取写锁,获取读锁,再释放写锁的次序。写锁可以降级成读锁

ReentrantReadWriteLock的实现的接口是ReadWriteLock:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

非常简单的接口,我们主要看ReentrantReadWriteLock是如何实现这些接口的。

ReentrantReadWriteLock源码分析

如果你还记得之前ReentrantLock的内容,应该也会记得Lock和AQS同步器有很深的关系。ReentrantReadWriteLock存在ReadLock和WriteLock,2种锁,那么他们也会有对应的Lock和自定义AQS同步器。

public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

Readlock和WriteLock会同时ReentrantReadWriteLock的同步器:

protected ReadLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;
}
protected WriteLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;
}

AQS的核心是锁的实现,即控制同步状态state的值,ReentrantReadWriteLock也是利用AQS这个特性来控制同步状态,那么接下来的主要问题是:
一个int类型的state怎么同时控制读的同步状态和写的同步状态

读写状态的设计

如果在一个int类型变量上维护多个状态,那么肯定需要利用到拆分。我们知道int类型数据占用32位,那么我们可以切割一下,使得:

  1. 高16位表示读
  2. 低16位表示写

ReentrantReadWriteLock特殊的Sync类:

private static final long serialVersionUID = 6317671515068378041L;

/*
    * Read vs write count extraction constants and functions.
    * Lock state is logically divided into two unsigned shorts:
    * The lower one representing the exclusive (writer) lock hold count,
    * and the upper the shared (reader) hold count.
    */

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

/** Returns the number of shared holds represented in count  */
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count  */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

这个看着很唬人,一堆位运算符,其实背后的数学原理很简单:

  1. 如果写状态+1,那么state+1
  2. 如果读状态+1,那么state+(1<<16)
  3. 如果state的值不等于0,当写状态等于0,则读状态一定大于0,反之依然

在我们正式介绍Java的ReentrantReadWriteLock之前,我想想说说Spark里面类似锁会碰到的一些问题

  1. 写饥饿。因为现实场景读多写少,所以写锁请求到来之后,必须记得停止读锁状态不等增加。否则写锁请求可能永远无法满足,造成写饥饿
  2. 锁降级的时候必须谨慎。现实状态中有很多写-读的请求。写更新状态,读来确定状态更新。如果锁降级的设计有问题,第二次读很有可能脏读。

总结

最近事情比较多,今天这一节就简短介绍一下ReentrantReadWriteLock的一些简单结构和锁状态管理。下一节我们会开始进行源码分析。

Leave a Reply

Your email address will not be published. Required fields are marked *