【读写锁原理分析】

类结构

读写锁对应ReadLock以及WriteLock,可以看作是一把锁的两个视图,现在分为读写成以及写线程。读线程之间不互斥,读线程和写线程互斥,写线程之间也互斥。
ReadWriteLock是一个接口,真正的实现是在ReentrantReadWriteLock中,如下类图:
在这里插入图片描述

ReentrantReadWriteLock构造函数

readerLock和writerLock实际公用一个sync对象,同互斥锁一样分为公平和非公平两种策略,都继承自AQS

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

同互斥锁一样,读写锁也是用state变量来表示锁的状态的,只是state变量的含义不同,在Sync中又对state变量重新定义:
把state拆成两半,高16位用来记录读锁,低16位来记录写锁。当state为0时,说明没有线程占有锁,当state不等于0时,有可能是写锁或者是读锁,两者不能同时成立,这时只能通过sharedCount和exclusiveCount方法判断到底是读锁还是写锁占用了改线程

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;

/** 持有读锁的线程重入次数  */
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
/** 持有写锁的线程重入次数  */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
读锁的公平与非公平锁的实现
acquireShared实现

查看读锁的代码,发现读锁是通过AQS的方法acquireShared方法来实现的,如下:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

这里的tryAcquireShared函数最终调用的是Sync类中的方法:

protected final int tryAcquireShared(int unused) {
    // 获取当前线程,以及state值
    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)) {//CAS高16位加1
        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);//读锁获取失败,进入该函数自旋拿锁
}

这段代码的主要逻辑有三个:
1.判断低16位不等于0,说明写线程持有锁,并且只有当ownerThread!=自己时,才会返回-1,实际上就是说,一个线程在持有了写锁后,可以再次获取读锁
2.compareAndSetState,其实是把高16位加一
3.readerShouldBlock公平锁以及非公平锁实现的差异,readerShouldBlock只是Sync中的一个模版方法,具体实现在FairSync以及NoneFairSync:

static final class FairSync extends Sync {
    //写线程抢锁时是否应该阻塞
    final boolean writerShouldBlock() {
        // 写线程在抢锁之前,如果如果队列中有其他线程在排队,就要阻塞,所以是公平的
        return hasQueuedPredecessors();
    }
    //读线程抢锁时是否应该阻塞
    final boolean readerShouldBlock() {
        // 读线程在抢锁之前,如果如果队列中有其他线程在排队,就要阻塞,所以是公平的
        return hasQueuedPredecessors();
    }
}
static final class NonfairSync extends Sync {
    //写线程抢锁时是否应该阻塞
    final boolean writerShouldBlock() {
        // 非公平,在阻塞之前要先抢锁
        return false;
    }
    //读线程抢锁时是否应该阻塞
    final boolean readerShouldBlock() {
        // 读线程抢锁时,当队列中的第一个元素是写线程,要阻塞
        return apparentlyFirstQueuedIsExclusive();
    }
}

对于公平锁,无论是读锁还是写锁,只要队列中有其他线程在排队就不能直接去抢,要排在队列尾部
对于非公平,写锁不管三七二十一上来就直接抢锁,而读锁不行,因为读线程和读线程是不互斥的,假设当前线程被读线程占用,其他读线程还一直不公平的抢锁,可能导致写线程永远拿不到锁,所以当发现队列第一个元素是写线程时,读线程应该被阻塞。

tryReleaseShared实现

因为读锁是共享锁,多个线程会同时持有锁,所以对读锁的释放不能直接减一,需要通过CAS操作不断重试

for (;;) {
    int c = getState();
    int nextc = c - SHARED_UNIT;
    if (compareAndSetState(c, nextc))
        return nextc == 0;
}
写锁的公平与非公平锁的实现
tryAcquire实现

查看写锁的代码,发现写锁是通过AQS的方法tryAcquire方法来实现的,如下:

protected final boolean tryAcquire(int acquires) {
    // 获取当前线程,以及state值
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
        if (w == 0 || current != getExclusiveOwnerThread())
            //w=0,说明有读锁或者持有写锁的线程不是自己,直接返回获取锁失败
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    // 公平与非公平的区别
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

代码分析如下:

  1. c != 0 and w == 0,说明当前一定是读线程拿锁,写锁一定拿不到,返回false
  2. c != 0 and w != 0,说明当前一定是写线程拿锁,执行current != getExclusiveOwnerThread(),发现不是自己,返回false
  3. c != 0 and w != 0 and current == getExclusiveOwnerThread(),才会走到重入次数的判断
  4. c == 0,当前没有线程占有锁,通过CAS抢占锁
tryRelease实现

因为写锁时排他的,在当前线程拥有写锁的时候,其他线程不既不会持有写锁,也不会持有读锁,所以这里的state操作不需要CAS,直接减一即可

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值