1.简介
并发中常用的ReentrantLock,是一种典型的排他锁,这类锁在同一时刻只允许一个线程进行访问,实际上将并行操作变成了串行操作。在并发量大的业务中,其整体效率、吞吐量不能满足实现的需要。而且实际的业务中一般情况是读多于写,多个线程读操作不会改变已经有的数据,不会有数据的一致性问题,而一个写操作就会改变数据,其他的的读操作就可能读到过期的数据。读写锁正是为了这种业务需求而产生的,读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
读写锁同时支持公平锁和非公平锁,默认实现是非公平锁,非公平锁的吞吐量高于公平锁。
读写锁也支持重入,读锁和写锁的最大重入次数是65535次,这是由int类型本身所能表达的范围区间所决定的。
读写锁能够实现锁降级,它按照先获取写锁、获取读锁再释放写锁的次序执行锁操作,而且写锁能够降级为读锁。
2.ReentrantReadWriteLock的类结构
从UML图可以看出ReentrantReadWriteLock中有多个内部类,这些内部类与ReentrantLock有些类似,都有Sync、NofairSync、FairSync,前者Sync继承于AQS,前者Sync是NofairSync、FairSync的父类。
与 ReentrantLock相比,虽然这3个静态内部类名字相同,但内部却有差异。ReentrantLock是排他锁,又可以说是一种写锁,这三个静态内部类只重写了AQS中的tryAcquire(int) 、tryRelease(int)这两个排他锁相关方法。而ReentrantReadWriteLock是读写锁,要实现排他锁和共享锁这两种锁,Sync、NofairSync、Fair不仅重写SynctryAcquire(int) 、tryRelease(int)方法,另外还重写了AQS中的tryAcquireShared(int)、 tryReleaseShared(int)这两个共享锁相关方法。
ReadLock是表示读锁的静态内部类,它主要委托NofairSync/FairSync中的共享锁相关方法实现的,如"void acquire(int)" "boolean release(int)"等(这两个方法是父类AQS的模板方法,模板方法再去调用自身重写的tryAcquire(int) 、tryRelease(int)方法)。
WriteLock是表示写锁的静态内部类,它主要委托NofairSync/FairSync中的排他锁相关方法实现的,如"void acquireShared(int)" "boolean releaseShared(int)"等。
HoldCounter和ThreadLocalHoldCounter又都是Sync的静态内部类,HoldCounter类的主要作用是记录获一个获取取到共享锁的读线程的重入次数,ThreadLocalHoldCounter主要是为每个获取取到共享锁的读线程单独维护一个HoldCounter。
3.ReadWriteLock的一此方法与示例
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类还有一些使我们更容易并发编程的一些辅助方法,这些方法都直接委托Sync(或其子类NofairSync/FairSync)去实现。
public int getReadLockCount() { return sync.getReadLockCount(); } public boolean isWriteLocked() { return sync.isWriteLocked(); } public boolean isWriteLockedByCurrentThread() { return sync.isHeldExclusively(); } public int getWriteHoldCount() { return sync.getWriteHoldCount(); } public int getReadHoldCount() { return sync.getReadHoldCount(); }
getReadLockCount()返回成功获取读锁的线程数,即此锁持有的读取锁的数量。。
isWriteLocked()返回写锁是否被某个线程成功获取了。
isWriteLockedByCurrentThread()返回写锁是否被当前线程成功获取了。
getWriteHoldCount()返回当前线程重复获取写锁(重入)的次数。
getReadHoldCount()返加当前线程重复获取读锁(重入)的次数。
4.保存读写状态的设计理念
ReentrantLock使用AQS中的int类型的state成员变量来保存排他锁状态,重入一次使state自增1,每释放一次重入状态state自减1(以前的帖子)。ReentrantReadWriteLock的重入设计理念应该也是基于此。ReentrantLock只需要保存写锁状态,直接使用state成员变量就可以实现,关键在于如何同时保存ReentrantReadWriteLock的读锁状态与写锁状态。其实可以借鉴CPU的标志寄存器的设计理念,将一个变量"按位分割",不同的位范围表示不同的含义。ReentrantReadWriteLock就是将state的高16位表示读状态、低16位表示写状态(int类型4个字节,共32比特位)。
利用位运算操作,可以分别将高16位和低16位的值取出来。
取低16位的写状态,只需要将state的高16位的所有位设为0即可,根据“0和(1或0)进行按位与操作结果都是0、(0或1)和1进行与操作结果均为其本身”的特点,可以使用按位与操作表达式"state&0x0000FFFF"实现,所以在Sync中exculusiveCount()方法体中有"c&EXCLUSIVE_MASK",而EXCLUSIVE_MASK等于0x0000FFFF。
取高16位的读状态,只需要将state的低16位的抹除即可,可将state无符号右移16位,所以Sync中的sharedCount方法体中有代码"c >>> SHARED_SHIFT"。
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; }
5.写锁的获取与释放
写锁的lock()方法实际调用Sync的父类AQS的acquire(int)方法,acquire(int)是模板方法,acquire(int)的主要逻辑在以前的帖子中分析过,我们重点关注被Sync重写的tryAcuqrie(int)方法,
public void lock() { sync.acquire(1); } public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
tryAcquire的基本逻辑:如果读锁被获取了或写锁被其他线程获取了,那么尝试获取写锁失败。如果在之前当前线程已经获取了写锁,增加重入次数,尝试获取写锁成功。若未有任何读锁、写锁被获取,则进行CAS更新state,若更新成功,尝试获取写锁成功,返回true,若更新失败,尝试获取写锁失败,返回false.
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) {//当前锁至少持有1个读锁或1个写锁(任何情况下最多只能有一个写锁) // (Note: if c != 0 and w == 0 then shared count != 0) /** * 因为 共享锁重入次数+排他锁重入次数 < state ,即 shareCout+ w<c,而又c>0 && w=0, * 那么shareCount>0,当前读锁被某线程获取了,所以这里w=0表示当前读锁被某些线程获取了, * 在读锁被获取了的情况下,不能获取写锁(只有在所有读锁、写锁均补充释放才能获取写锁),返回false。 * * "current != getExclusiveOwnerThread()"为true表明, * 前置条件"w==0"不成立,那么w>0,即写锁已经被某线程获取到了, * 而"current != getExclusiveOwnerThread()"条件本身又表明,当前线程不是获取到写锁的线程 * 所以尝试获取写锁失败,返回false * * 综合起来说,当有读锁被某些线程成功获取或写锁被其他线程成功获取时, * 尝试获取写锁失败,返回false。 */ if (w == 0 || current != getExclusiveOwnerThread()) return false; if (w + exclusiveCount(acquires) > MAX_COUNT) //超出了最大可重入次数,MAX_COUNT=0x0000FFFF throw new Error("Maximum lock count exceeded"); // Reentrant acquire //之前写锁已经被当前线程成功获取,重入次数自增 //尝试获取写锁成功,返回true setState(c + acquires); return true; } //getState=0,当前锁不持有任何读锁、写锁 if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; //cas更新失败,尝试获取写锁失败,返回false //cas更新成功,设置当前线程为独占线程,尝试获取写锁成功,,返回false setExclusiveOwnerThread(current); return true; }
这里与ReentrantLock不同的地方在于多了对读锁是否存在的判断。读锁的读取线程进行读取操作时,它不能主动感知写入操作,如果同时进行读写操作,读入的数据可能是被删除的数据或是过期的数据,读取时不能写入,所以在已有读锁的时候不能再去获取写锁。反过来也是一样,为保证数据的一致性,在有写锁的时候,它要阻塞其他所有的读写操作,不能再去获取任何读锁和写锁。
tryRelease(int)尝试释放锁的基本逻辑和ReentrantLock几乎一样:将重入次数自减,当重入次数为0,将独占线程设为null,返回true,反之返回false.
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; }
原文地址:https://www.cnblogs.com/gocode/p/12310430.html