读写锁 -- ReentrantReadWriteLock

1. 类继承关系

在这里插入图片描述
如图所示, ReadWriteLock 是一个接口,内部由两个Lock 接口组成。

public interface ReadWriteLock {
    Lock readLock();
    Lock writeLock();
}

ReentrantReadWriteLock 实现了该接口,使用方式如下:

ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock rLock = rwLock. readLock();
rLock.lock();
rLock.unlock();
Lock wLock = rwLock.writeLock();
wLock.lock();
wLock.unlock();

也就是说,当使用ReadWriteLock 的时候, 并不是直接使用,而是获得其内部的读锁和写锁,然后分别调用lock/unlock 。

2. 读写锁实现的基本原理

从表面来看, ReadLock 和WriteLock 是两把锁,实际上它只是同一把锁的两个视图而己。什么叫两个视图呢?可以理解为是一把锁,线程分成两类:读线程和写线程。读线程和读线程之间不互斥(可以同时拿到这把锁〉,读线程和写线程互斥,写线程和写线程也互斥。
从下面的构造函数也可以看出, readerLock 和writerLock 实际共用同一个sync 对象。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 变量进行了重新定义,如下所示。

abstract static class Sync extends AbstractQueuedSynchronizer {
/*
         * 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; }
}

也就是把state 变量拆成两半,低16 位,用来记录写锁。但同一时间既然只能有一个线程写,为什么还需要16 位呢?这是因为一个写线程可能多次重入。例如,低16 位的值等于5 ,表示一个写线程重入了5 次。
高16 位,用来“读”锁。例如,高16 位的值等于5 ,可以表示5 个读线程都拿到了该锁;也可以表示一个读线程重入了5 次。
这个地方的设计很巧妙, 为什么要把一个int 类型变量拆成两半,而不是用两个int 型变量分别表示读锁和写锁的状态呢?这是因为无法用一次CAS 同时操作两个int 变量,所以用了一个int 型的高16 位和低16 位分别表示读锁和写锁的状态。
当state = 0 时,说明既没有线程持有读锁,也没有线程持有写锁:当state != 0 时,要么有线程持有读锁,要么有线程持有写锁,两者不能同时成立,因为读和写互斥。这时再进一步通过sharedCount(state)和exclusiveCount(state)判断到底是读线程还是写线程持有了该锁。

3. AQS 的两对模板方法

下面介绍在ReentrantReadWriteLock 的两个内部类ReadLock 和WriteLock中,是如何使用state 变量的。

public static class ReadLock implements Lock, java.io.Serializable {
	public void lock() {
        sync.acquireShared(1);
    }
    public void unlock() {
        sync.releaseShared(1);
    }
    ...
}

public static class WriteLock implements Lock, java.io.Serializable {
	public void lock() {
        sync.acquire(1);
     }
     public void unlock() {
        sync.release(1);
    }
    ...
}

acquire / release 、acquireShared/releaseShared 是AQS 里面的两对模板方法。互斥锁和读写锁的写暂且都是基于acquire /release 模板方法来实现盹读写锁的读暂且是基于acquireShared/ releaseShared这对模板方法来实现的。这两对模板方法的代码如下:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
	implements java.io.Serializable {
	public final void acquire(int arg) {
        if (!tryAcquire(arg) &&				// tryAcquire被各种Sync子类实现
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
	public final boolean release(int arg) {
        if (tryRelease(arg)) {				// tryRelease被各种Sync子类实现
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)		// tryAcquireShared被各种Sync子类实现
            doAcquireShared(arg);
    }
    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {		// tryReleaseShared被各种Sync子类实现
            doReleaseShared();
            return true;
        }
        return false;
    }
}

将读/写、公平/非公平进行排列组合,就有4 种组合。如图下图所示,上面的两个函数都是在Sync 中实现的。Sync 中的两个函数又是模板方法,在NonfairSync 和FairSync 中分别有实现。最终的对应关系如下:

(1)读锁的公平实现: Sync.tryAccquireShared() + FairSync 中的两个覆写的子函数。
(2)读锁的非公平实现: Sync.tryAccquireShared() + NonfairSync 中的两个覆写的子函数。
(3)写锁的公平实现: Sync.tryAccquire() + FairSync 中的两个覆写的子函数。
(4)写锁的非公平实现: Sync.tryAccquire() + NonfairSync 中的两个覆写的子函数。
在这里插入图片描述

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -8159625535654395037L;
        final boolean writerShouldBlock() {		            	// 写线程抢锁的时候是否应该阻塞,
            return false; 										// 写线程在抢锁之前永远不被阻塞,是非公平的
        }
        final boolean readerShouldBlock() {						// 读线程抢锁的时候是否应该阻塞,
            return apparentlyFirstQueuedIsExclusive();			// 读线程抢锁的时侃当队列中第1个元素是写线程的时候,要阻塞
        }
    }
    static final class FairSync extends Sync {
        final boolean writerShouldBlock() {			// 写线程抢锁的时候是否应该阻塞
            return hasQueuedPredecessors();			// 写线程在抢锁之前,如果队列里有其他线程在排队,就要阻塞,所以是公平的
        }
        final boolean readerShouldBlock() {		// 读线程抢锁的时候是否应该阻塞
            return hasQueuedPredecessors();		// 读线程在抢锁之前,如果队列里有其他线程在排队,就要阻塞,所以是公平的
        }
    }

上面的代码介绍了ReentrantReadWriteLock 里面的NonfairSync 和FairSync 的实现过程,对应了上面的四种实现策略,下面分别解释。
对于公平,比较容易理解,不论是读锁,还是写锁,只要队列中有其他线程在排队(排队等读锁,或者排队等写锁),就不能直接去抢锁,要排在队列尾部。
对于非公平,读锁和写锁的实现策略略有差异。先说写锁,写线程能抢锁,前提是state= 0,只有在没有其他线程持有读锁或写锁的情况下,它才有机会去抢锁。或者state != 0 ,但那个持有写锁的线程是它自己,再次重入。写线程是非公平的,就是不管三七二十一就去抢,即一直
返回false 。
但对于读线程,能否也不管三七二十二,上来就去抢呢?不行!因为读线程和读线程是不互斥的,假设当前线程被读线程持有,然后其他读线程还非公平地一直去抢,可能导致写线程永远拿不到锁,所以对于读线程的非公平,要做一些“约束”。当发现队列的第l 个元素是写线程的时候,读线程也要阻塞一下, 不能“肆无忌惮”地直接去抢。
明白策略后,下面具体介绍四种实现方面的差异。

4. WriteLock公平与非公平实现

写锁是排他锁,实现策略类似于互斥锁,重写了tryAcquire/tryRelease 方法。

4.1 tryAcquire()实现分析
	protected final boolean tryAcquire(int acquires) {
	   /*
	     * Walkthrough:
	     * 1. If read count nonzero or write count nonzero
	     *    and owner is a different thread, fail.
	     * 2. If count would saturate, fail. (This can only
	     *    happen if count is already nonzero.)
	     * 3. Otherwise, this thread is eligible for lock if
	     *    it is either a reentrant acquire or
	     *    queue policy allows it. If so, update state
	     *    and set owner.
	     */
	    Thread current = Thread.currentThread();
	    int c = getState();
	    int w = exclusiveCount(c);
	    if (c != 0) {
	        // (Note: if c != 0 and w == 0 then shared count != 0)
	        if (w == 0 || current != getExclusiveOwnerThread())
	            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) if(c !=0) and w== 0 ,说明当前一定是读线程拿着锁,写锁一定拿不到,返回false ,
(2) if (c != 0) and w != 0 ,说明当前一定是写线程拿着锁,执行current != getExclusiveOwnerThread()的判断,发现ownerThread
不是自己,返回false 。
(3) c != o, w != 0 ,且current = getExclusiveOwnerThread(),才会走到if (w + exclusiveCount(acquires) > MAX_COUNT) 。
判断重入次数,重入次数超过最大值,抛出异常。
因为是用state 的低16位保存写锁重入次数的,所以MAXCOUNT 是2的16次方 。如果超出这个值, 会写到读锁的高16 位上。为了避免
这种情形,这里做了一个检测。当然, 一般不可能重入这么多次。
(4) if(c = 0),说明当前既没有读线程,也没有写线程持有该锁。可以通过CAS 操作开抢了。

if (writerShouldBlock ()  || !compareAndSetState(c, c + acquires))

抢成功后,调用setExclusiveOwnerThread(current),把ownerThread 设成自己。
公平实现和非公平实现几乎一模一样,只是writerShouldBlock()分别被FairSync 和NonfairSync 实现,在上一节己讲。

4.2 tryRelease(…)实现分析
	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);			//  因为写锁是排他的,在当前线程持有写锁的时候,其他线程既不会持有写锁,
        							// 不会持有读锁。所以,这里对state 值的调减不需要CAS 操作,直接减1即可
        return free;
    }

5. ReadLock公平与非公平实现

读锁是共享锁,重写了tryAcquireShared/tryReleaseShared方法,其实现策略和排他锁有很大的差异。

5.1 tryAcquireShared(…)实现分析
	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)) {		// CAS 拿读锁,高16 位加1
	        if (r == 0) {		// 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)

if (exclusiveCount (c) != 0 && getExclusiveOwnerThread () != current)
	return - 1 ;

低16 位不等于0 ,说明有写线程持有锁,并且只有当ownerThread != 自己时,才返回-1 。这里面有一个潜台词:如果current = ownerThread ,则这段代码不会返回。这是因为- 个写线程可以再次去拿读锁!也就是说, 一个线程在持有了WriteLock 后,再去调用ReadLock.lock 也是可以的。
(2) 上面的compareAndSetState(c, c + SHARED_ UNIT), 其实是把state 的高16 位加1 (读锁的状态),但因为是在高16 位,必须把1左移16 位再加1 。
(3) firstReader, cachedHoldConunter 之类的变量,只是一些统计变量, 在ReentrantReadWriteLock对外的一些查询函数中会用到,例如,查询持有读锁的线程列表,但对整个读写互斥机制没有影响,此处不再展开解释。

5.2 tryReleaseShared(…)实现分析
protected final boolean tryReleaseShared(int unused) {
            Thread current = Thread.currentThread();
            ...
            for (;;) {
                int c = getState();
                int nextc = c - SHARED_UNIT;
                if (compareAndSetState(c, nextc))
                    // Releasing the read lock has no effect on readers,
                    // but it may allow waiting writers to proceed if
                    // both read and write locks are now free.
                    return nextc == 0;
            }
        }

因为读锁是共享锁,多个线程会同时持有读锁,所以对读锁的释放不能直接减1 ,而是需要通过一个for 循环+ CAS 操作不断重试。这是tryReleaseShared 和tryRelease 的根本差异所在。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值