Java并发编程之ReentrantReadWriteLock(一)

34 阅读3分钟

简介

前面介绍的ReentrantLock,只要有线程加锁了其他线程就只能进行等待,但是如果多个线程都只是读取数据并没有修改数据的话,完全可以不用加锁。而ReentrantReadWriteLock就是采用了读写分离的模式,根据操作类型分别加读锁或写锁,大大提高了读操作的效率,实现了读读并发、读写互斥的效果。

使用

public class ReadWriteLockDemo {
    private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
    private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
    private static int data;
    public static void main(String[] args) {
        new Thread(() -> {
            readLock.lock();
            try {
                System.out.println("读锁操作 " + data);
            } finally {
                readLock.unlock();
            }
        }, "t1").start();
        new Thread(() -> {
            writeLock.lock();
            try {
                System.out.println("写锁操作");
                data = 10;
            } finally {
                writeLock.unlock();
            }
        }, "t1").start();
    }
}

ReentrantReadWriteLock的使用方法很简单,先创建一个ReentrantReadWriteLock对象,然后分别创建一个 读锁ReadLock和写锁WriteLock对象,加锁时调用lock方法,解锁时调用unlock方法。

读锁

加锁流程
// ReentrantReadWriteLock.ReadLock类
public void lock() {
    sync.acquireShared(1);
}
// AQS类
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

ReadLock类的lock方法调用的是AQSacquireShared方法,这个方法的大致意思是先尝试去获取共享锁,如果成功了就结束,如果失败了就把当前线程添加到等待队列中,不断进行重试和等待。

尝试加锁
protected final int tryAcquireShared(int unused) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;                                                   // 第一步
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;                                                    // 第二步
    }
    return fullTryAcquireShared(current);                            // 第三步
}

分析这个方法之前先介绍下这里面一些属性和方法的含义:

  • firstReader:第一个获取读锁的线程
  • firstReaderHoldCount:第一个获取读锁的线程持有的读锁数量(重入数量)
  • cachedHoldCounter:上一个持有读锁的线程持有的读锁数量
  • readHolds:通过ThreadLock存储的本线程持有的读锁数量

ReentrantReadWriteLock是通过state属性来存储读写锁数量的,高16位表示共享锁数量,低16位表示独占锁数量

  • exclusiveCount(state):获取独占锁(写锁)数量
  • sharedCount(state):获取共享锁(读锁)数量

再回到tryAcquireShared方法,第一步首先获取独占锁数量,如果不是0表示有线程加了写锁,然后判断持有锁的线程是不是当前线程,不是的话说明其他线程加了写锁,当前线程加读锁就失败了,直接返回-1

如果没有其他线程加写锁就进入第二步,首先调用readerShouldBlock方法判断读锁是否要进行阻塞,源码如下:

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

这里的四个表达式意思分别是:

  • head节点不是null,阻塞队列初始化时会创建一个空节点作为head节点,真正阻塞等待的节点是从第二个节点开始;
  • 第二个节点不是null
  • 第二个节点不是以共享模式进行阻塞的,也就是独占模式;
  • 第二个节点的线程不是null; 简言之,如果返回true表示队列中第二个节点(也就是有资格获取锁的下一个节点)要加写锁。第二步条件中进行了取反操作就直接进入第三步了;如果不满足这四个表达式的任意一种情况就继续判断第二个条件。

第二个条件判断的是共享锁的数量是不是达到了最大值,达到了就执行第三步,否则继续判断第三个条件;

第三个条件尝试CAS修改state的值,对共享锁数量进行加1

如果这三个条件都成功了,也就意味着当前线程获取到了读锁,那么继续进行后续操作。

if (r == 0) {
    firstReader = current;
    firstReaderHoldCount = 1;
} 

首先判断r==0,也就是共享锁数量是否为0,如果成立表示之前没有其他线程加读锁,当前线程就是获取到读锁的第一个线程,然后把firstReader标记为当前线程,并把持有锁数量设为1

else if (firstReader == current) {
    firstReaderHoldCount++;
}

如果已经有线程获取到读锁了,就判断firstReader是不是当前线程,如果是表示发生了重入,直接执行firstReaderHoldCount++修改持有锁数量。

HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
    cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
    readHolds.set(rh);
rh.count++;

如果第一个持有锁的线程不是当前线程,首先获取上一个获取到读锁的线程持有锁的数量,如果是null或者上一个线程不是当前线程,就把当前线程标记为最后一个线程,readHolds.get()获取本线程持有锁数量然后赋值给cachedHoldCounter

如果上一个获取读锁的线程是当前线程,并且持有锁的数量变成了0,就更新本线程的持有锁数量。

最后通过rh.count++把锁数量加1。

第三步及后续流程将在下一篇文章中进行介绍。