ReadWriteLock

版本JDK7

看本文之前,需要了解 AQSReentrantLock。下文如果不做特别说明,AQS 都是指 AbstractQueuedSynchronizer。

1. 特性

ReadWriteLock 翻译过来就是读写锁。读写锁在 同一时刻 允许多个线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。

使用场景:读多写少的环境

ReentrantReadWriteLock 是目前官方 JDK 提供 ReadWriteLock 的唯一实现。

ReentrantReadWriteLock 的特性 说明
公平性的选择 支持非公平(默认)和公平的锁获取方式,就吞吐量而言,非公平要优于公平
重进入 该锁支持重进入。以读写线程为例:读线程在获取了读锁之后,(获取读锁的线程)该线程能够再次获取读锁;而写线程在获取写锁之后,能够再次获取写锁,同时也能获取读锁。
锁降级 遵循下面的次序:1.获取写锁 2. 获取读锁 3.释放写锁。写锁就能降级成为读锁,反之不行。

ReentrantReadWriteLock 展示内部工作状态的方法

方法名称 描述
int getReadLockCount() 返回当前读锁被获取的次数。该次数不等于获取读锁的线程数,例如,仅一个线程, 它连续获取了 n 次读锁,那么占有读锁的线程数是 1,但该方法返回 n。
int getReadHoldCount() 返回当前线程获取读锁的次数。使用 ThreadLocal 保存当前线程获取的次数。
boolean isWriteLock() 判断写锁是否被获取
int getWriteHoldCount() 返回当前写锁被获取的次数

2. 解读 ReentrantReadWriteLock

ReentrantReadWriteLock 是 Java 并发包里面提供的唯一的一个读写锁实现。

ReadWriteLock 提供了2个抽象方法 readLock() 和 writeLock() 用于获取读锁和写锁。

下面就让我们来看看这个复杂的锁。

2.1 ReentrantReadWriteLock 的结构

从上面的截图中可以看出,ReentrantReadWriteLock 中有很多的内部静态类。下面我们从这几个内部类来熟悉一下其内部结构。

2.1.1 构造方法

ReentrantReadWriteLock 提供了2个构造方法:

public ReentrantReadWriteLock() {
    this(false);
}

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

从上面的代码可以看出,这2个构造方法决定了 ReentrantReadWriteLock 的获取是否是公平性的,同时初始化了 ReadLock 和 WriteLock。

有趣的是,FairSync 与 NonfairSync 之间的唯一不同之处就是各自实现了父类 Sync 的 readerShouldBlock 和 writerShouldBlock 这两个抽象方法(暂时不必关心,后面后面会介绍)。

2.1.2 ReadLock 与 WriteLock

下面来看看 ReadLock 和 WriteLock 这两个类的结构。

注意:ReadLock 与 WriteLock 采用的是两种完全不同的 lock 方式。

类型
ReadLock 共享模式 - acquireShared(1)
WriteLock 独占模式 - acquire(1)

2.1.3 小结

  1. Sync 是 AbstractQueuedSynchronizer 的子类,然后再衍生出了公平模式和非公平模式。

  2. Sync 重写了 AbstractQueuedSynchronizer 的 tryRelease、tryAcquire、tryReleaseShared、tryAcquireShared 和 isHeldExclusively 这5个方法。Sync 既能提供独占式获取与释放锁的功能,也拥有共享式获取与释放锁的功能。

  3. ReadLock 和 WriteLock 的内部方法几乎都是由 Sync 来实现的。但是 ReadLock 和 WriteLock 分别继承 Sync 的不同获取锁的方式:ReadLock - 共享式;WriteLock - 独占式。

  4. ReadLock 和 WriteLock 一样,都是有公平模式和非公平模式。

2.2.1 Sync - 读、写的同步状态获取方式

前面我们介绍过 Sync 的结构,知道 Sync 是同时拥有独占锁和共享锁的。但是 Sync 是如何快速确定读和写的各自的状态呢?答案就是通过位运算。

abstract static class Sync extends AbstractQueuedSynchronizer {
    
    ... ...

    static final int SHARED_SHIFT   = 16;
    static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
    // MAX_COUNT == 2^16 - 1,即读写锁各自最大的同步状态数
    static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
    // 转换成二进制 == 00000000 00000000 11111111 11111111
    static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

    // 读的同步状态为高16位
    static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
    // 写的同步状态的第16位
    static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

    ... ...
}

假设当前同步状态为S,

2.2.2 Sync - 独占式获取与释放的具体实现

下面可能会出现独占锁和写锁这两个词,如果不做特殊说明,这两个就是一个意思。

hasQueuedPredecessors() 解析

前面解析写锁的获取过程的时候,我们可以看到,在公平模式下面,writerShouldBlock() 这个方法内部调用了 hasQueuedPredecessors()。下面,让我们来看看 hasQueuedPredecessors 是如何来判断当前同步队列中是否有线程等待时间长于当前线程。

public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

代码量很少,但是想要看明白却不太简单。

我归纳了一下 hasQueuedPredecessors 返回值所对应的几种情况:

简单画了一个图来表示:

2.2.3 Sync - 共享式获取与释放锁的具体实现

如果不做特殊说明,共享锁与读锁代表同一个意思。

apparentlyFirstQueuedIsExclusive() 解析

final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&
        s.thread != null;
}

apparentlyFirstQueuedIsExclusive 方法就是用来检查同步队列中head的后继节点是否为独占模式。

ReentrantReadWriteLock 的作者 Doug Lea 给写锁更高的优先级:如果遇上获取写锁的线程马上就要获取到锁了(head.next为获取写锁的线程),获取读锁的线程不应该和它抢。否则,可以随便抢。

3. 使用ReentrantReadWriteLock

  1. 锁降级
public void processData() {
    readLock.lock();
    if(!update) {
        // 必须先释放读锁
        readLock.unlock();
        // 锁降级从写锁获取到开始
        writeLock.lock();
        try {
            if(!update) {
                // 装备数据的流程(略)
                update = true;
            }
            // 再获取读锁
            readLock.lock();
        } finally {
            // 写锁释放
            writeLock.unlock();
        }
        // 锁降级完成
    }
    try {
        // 使用数据的流程(略)
    } finally {
        readLock.unlock();
    }
}
  1. 下面是一个对ArrayList添加并发功能的例子。

    ReadWriteList.java

     public class ReadWriteList<E> {
    
         private List<E> list = new ArrayList<>();
         private ReadWriteLock rwLock = new ReentrantReadWriteLock();
    
         public ReadWriteList(E... initialElements) {
             list.addAll(Arrays.asList(initialElements));
         }
    
         public void add(E element) {
             Lock writeLock = rwLock.writeLock();
             writeLock.lock();
    
             try {
                 list.add(element);
             } finally {
                 writeLock.unlock();
             }
         }
    
         public E get(int index) {
             Lock readLock = rwLock.readLock();
             readLock.lock();
    
             try {
                 return list.get(index);
             } finally {
                 readLock.unlock();
             }
         }
    
         public int size() {
             Lock readLock = rwLock.readLock();
             readLock.lock();
    
             try {
                 return list.size();
             } finally {
                 readLock.unlock();
             }
         }
    
     }
    

    如你所见,该类将 ArrayList 包装为底层数据结构。ReadWriteList 使用读锁来保护对读操作的并发访问(get()和size()方法),并使用写锁来保护对写操作的并发访问(add()方法)。

    下面创建一个写线程类往 List 中添加 100 以内的随机数。

    Writer.java

     public class Writer extends Thread {
         private ReadWriteList<Integer> sharedList;
        
         public Writer(ReadWriteList<Integer> sharedList) {
             this.sharedList = sharedList;
         }
        
         public void run() {
             Random random = new Random();
             int number = random.nextInt(100);
             sharedList.add(number);
        
             try {
                 Thread.sleep(100);
                 System.out.println(getName() + " -> put: " + number);
             } catch (InterruptedException ie ) { ie.printStackTrace(); }
         }
     }
    

    再创建一个读线程来随机访问 List 已有的元素。

    Read.java

     public class Reader extends Thread {
         private ReadWriteList<Integer> sharedList;
    
         public Reader(ReadWriteList<Integer> sharedList) {
             this.sharedList = sharedList;
         }
    
         public void run() {
             Random random = new Random();
             int index = random.nextInt(sharedList.size());
             Integer number = sharedList.get(index);
    
             System.out.println(getName() + " -> get: " + number);
    
             try {
                 Thread.sleep(100);
             } catch (InterruptedException ie) {
                 ie.printStackTrace();
             }
    
         }
     }
    

    下面是测试代码:

    ReadWriteLockTest.java

     public class ReadWriteLockTest {
         static final int READER_SIZE = 10;
         static final int WRITER_SIZE = 2;
        
         public static void main(String[] args) {
             Integer[] initialElements = {33, 28, 86, 99};
        
             ReadWriteList<Integer> sharedList = new ReadWriteList<>(initialElements);
        
             for (int i = 0; i < WRITER_SIZE; i++) {
                 new Writer(sharedList).start();
             }
        
             for (int i = 0; i < READER_SIZE; i++) {
                 new Reader(sharedList).start();
             }
        
         }
     }
    

4. ReentrantLock 与 ReentrantReadWriteLock 之间的区别

参考

BACK