Java多线程系列--“JUC锁”08之 共享锁和ReentrantReadWriteLock
ReadWriteLock 和 ReentrantReadWriteLock介绍
ReadWriteLock,顾名思义,是读写锁。它维护了一对相关的锁 — — “读取锁”和“写入锁”,一个用于读取操作,另一个用于写入操作。
“读取锁”用于只读操作,它是“共享锁”,能同时被多个线程获取。
“写入锁”用于写入操作,它是“独占锁”,写入锁只能被一个线程锁获取。
注意:不能同时存在读取锁和写入锁!可以"读/读",但不能"读/写"、"写/写"。
ReadWriteLock是一个接口。ReentrantReadWriteLock是它的实现类,ReentrantReadwriteLock里面同时封装了读锁和写锁,分别为ReadLock、WriteLock。同时并发操作的时候、读读不互斥,是可以共享的。但是读写、写写操作是互斥的。它主要是通过读读操作不互斥,来减少锁的冲突,提升并发的性能。
示例:
public class ReentrantReadWriteLockDemo { private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); // 读锁 private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock(); // 写锁 private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock(); private static int value = 0; // 读取value的时候加读锁 public static int readValue() { try { readLock.lock(); return value; } finally { readLock.unlock(); } } // 修改value的时候加写锁 public static void addValue() { try { writeLock.lock(); value++; } finally { writeLock.unlock(); } } public static class ReadThread extends Thread { @Override public void run() { for (int i = 0 ; i < 300; i++) { System.out.println(readValue()); } } } public static class WriteThread extends Thread{ @Override public void run() { for (int i = 0 ; i < 100; i++) { addValue(); } } } public static void main(String[] args) throws InterruptedException { // 两个读线程,读读不互斥 ReadThread readThread1 = new ReadThread(); ReadThread readThread2 = new ReadThread(); // 一个写线程 WriteThread writeThread = new WriteThread(); readThread1.start(); readThread2.start(); writeThread.start(); // 主线程等待,readThread1、readThread2、writeThread线程运行结束之后再继续 readThread1.join(); readThread2.join(); writeThread.join(); System.out.println("结束"); } }
两个线程readThread1、readThread2读取数据的时候加读锁,这个是可以共享的。这样可以减少一部分锁冲突,提升整体的并发能力。
ReentrantReadWriteLock 内部有什么属性
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { // 读锁 private final ReentrantReadWriteLock.ReadLock readerLock; // 写锁 private final ReentrantReadWriteLock.WriteLock writerLock; // 同步器,读锁、写锁都是基于这个同步器来进行封装的 final Sync sync; }
我们分析一下这些属性的作用:
readLock:读锁,这里就是ReentrantReadWriteLock提供的一把读锁。
writeLock:写锁,这里就是ReentrantReadWriteLock提供的一把写锁。
sync:同步器,继承自AQS,读写锁的逻辑由Sync同步器来实现,上面的读锁、写锁只是对它的封装而已。
ReentrantReadWriteLock 构造函数
我们再来看一下ReentrantReadWriteLock的构造函数:
public ReentrantReadWriteLock() { this(false); } public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); }
看上面的构造函数中,sync居然也是有公平锁FairSync、NonfairSync非公平锁的概念。
如果默认构造函数传递boolean fair = false,也就是默认是非公平锁。
同时构造函数中,会同时创建一把读锁ReadLock、一把写锁WriteLock。
ReentrantReadWriteLock内部类结构
上面搞清楚了ReenatrantReadWriteLock内部有一把读锁、一把写锁,还有一个同步器Sync(公平模式、非公平模式)。现在我们先从整体上看一下ReentrantReadWriteLock内部类结构。
它的内部类结构跟我们之前讲过的ReentrantLock、Semaphore非常类似,也是有公平锁FairSync、非公平锁NonfairSync,并且它们都是继承自Sync,而Sync又继承了AQS,底层都是基于AQS来实现的。
其实啊ReentrantReadWriteLock的大部分逻辑都是封装在了Sync这个同步器里面了。
FairSync、NonfairSync这两个子类只是封装了公平、非公平的实现而已。
之前我们讲过很多次了,所谓公平的实现就是获取锁之前,查看AQS等待队列是否有人在排队,如果有人在排队,则自己不获取锁,去队列中等待。非公平的实现就是,上来就抢,不管有没有人在排队,抢到就返回,抢不到就去等待队列排队。
这里公平、非公平的实现还记得不?在讲解ReentrantLock章节、Semaphore章节的时候都讲解过了。
上面啊,都是在讲解ReentrantReadWriteLock整体内部有哪些属性、内部的类结构是怎么样子的,这些知识点都记住了没?
上面大概讲的就是:
(1)属性:有一把读锁readLock、一把写锁writeLock,一个抽象同步器Sync,其中锁的大部分逻辑都是封装在Sync这个抽象同步器里面;ReadLock、WriteLock都是对Sync进行了封装而已。
(2)锁模式和类结构:ReentrantReadWriteLock有公平锁、非公平锁两种模式,具体是根据FairSync、NonfairSync这两个同步工具类封装的,而FairSync、NonfairSync这两个同步工具类又继承自Sync,读写锁的逻辑大多数都封装在Sync里面。
从整体了解ReentrantReadWriteLock大概是这样的。我们接下来看看ReentrantReadWriteLock分别使用什么表示读锁,使用什么表示写锁,具体就在Sync这个抽象同步器里面。
int类型的32位数字同时表示写锁和读锁(高16位读锁、低16位写锁)
我们看下Sync这个类里面有什么属性,到底是怎么表示读锁,怎么表示写锁的?
abstract static class Sync extends AbstractQueuedSynchronizer { // 共享锁(读锁)的偏移量16位 static final int SHARED_SHIFT = 16; // 共享锁的单位 static final int SHARED_UNIT = (1 << SHARED_SHIFT); // 共享锁的最大个数,2的16次方-1 = 65535 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 独占锁掩码,65535,转化为二进制为 0000 0000 0000 0000 1111 1111 1111 1111 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; } }
上面的那些属性和方法,ReentrantReadWriteLock使用一个4个字节int 类型的数字同时表示读锁、写锁,int类型数字是4个字节,也就是32位,其中高16位表示读锁,低16位表示写锁。如下图所示:
那使用一个32位的数字,高16位表示读锁个数,低16位表示写锁个数;那我怎么知道当前加了多少个读锁,有没有人加写锁呢?
读锁和写锁个数怎么进行计算
老王:这个很简单哦,进行位运算不就可以了,比如说当前有个32位的数字,在二进制表示是这样的。
0000 0000 1010 0000 0000 0000 0110 1100,那么可以这样进行运算得到高低16位各自的加锁个数:
读锁的计算,直接将int类型的数字进行无符号右移16位即可:
对应到底层的方法源码就是:
static int sharedCount(int c) {
// 这里的SHARD_SHIFT就是16
// 就是将c进行无服务右移16位,得到读锁个数
return c >>> SHARED_SHIFT;
}
由于使用高16位表示读锁,所以读锁的个数最多为:2的16次方 - 1 = 65535。也就是如下变量:
// 高16位全部为1的时候, 1111 1111 1111 1111 也就是最大读锁个数
// 转化为十进制为65535个
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
上面就是读锁的计算,以及读锁最大的个数了,这个你能看懂吗?
这个我开始觉得很玄妙,原来也就是进行一下位运算而已啊,使用一个int类型的数字同时表示读锁、写锁,想要获取当前读锁的个数,直接将int类型的数字直接右移16位就可以了,很简单嘛。
说完了读锁怎么表示和计算;我再来给你说说写锁是怎么计算出来的,也还是以上面的那个int类型的32位数字为例讲解:
计算写锁的个数,也就是计算int类型的数字中低16位的结果是多少,也就是需要保留低16位的值,高16位全部置为0;对应到位运算的逻辑就是如下代码:
static int exclusiveCount(int c) {
// 只需要于写锁掩码 0000 0000 0000 0000 1111 1111 1111 1111
// 进行按位 & 运算即可
return c & EXCLUSIVE_MASK;
}
这个写锁的计算你明白了没?
很清楚了,其实就是保留一下低16位的结果,计算低16位的结果对应的十进制是多少,那么写锁的个数就是多少,这很简单咯。
就是这么简单。不过它这里读写锁的设计还是很精妙的,很多优秀的设计都是根据位运算的设计的。
后面我们要讲解的线程池中,也是使用一个int类型的数字能同时表示线程池的状态,线程池中线程个数,高3位表示线程池状态,低29位表示线程池中线程个数。
ReadWriteLock 和 ReentrantReadWriteLock函数列表
ReadWriteLock函数列表
// 返回用于读取操作的锁。 Lock readLock() // 返回用于写入操作的锁。 Lock writeLock()
// 创建一个新的 ReentrantReadWriteLock,默认是采用“非公平策略”。 ReentrantReadWriteLock() // 创建一个新的 ReentrantReadWriteLock,fair是“公平策略”。fair为true,意味着公平策略;否则,意味着非公平策略。 ReentrantReadWriteLock(boolean fair) // 返回当前拥有写入锁的线程,如果没有这样的线程,则返回 null。 protected Thread getOwner() // 返回一个 collection,它包含可能正在等待获取读取锁的线程。 protected Collection<Thread> getQueuedReaderThreads() // 返回一个 collection,它包含可能正在等待获取读取或写入锁的线程。 protected Collection<Thread> getQueuedThreads() // 返回一个 collection,它包含可能正在等待获取写入锁的线程。 protected Collection<Thread> getQueuedWriterThreads() // 返回等待获取读取或写入锁的线程估计数目。 int getQueueLength() // 查询当前线程在此锁上保持的重入读取锁数量。 int getReadHoldCount() // 查询为此锁保持的读取锁数量。 int getReadLockCount() // 返回一个 collection,它包含可能正在等待与写入锁相关的给定条件的那些线程。 protected Collection<Thread> getWaitingThreads(Condition condition) // 返回正等待与写入锁相关的给定条件的线程估计数目。 int getWaitQueueLength(Condition condition) // 查询当前线程在此锁上保持的重入写入锁数量。 int getWriteHoldCount() // 查询是否给定线程正在等待获取读取或写入锁。 boolean hasQueuedThread(Thread thread) // 查询是否所有的线程正在等待获取读取或写入锁。 boolean hasQueuedThreads() // 查询是否有些线程正在等待与写入锁有关的给定条件。 boolean hasWaiters(Condition condition) // 如果此锁将公平性设置为 ture,则返回 true。 boolean isFair() // 查询是否某个线程保持了写入锁。 boolean isWriteLocked() // 查询当前线程是否保持了写入锁。 boolean isWriteLockedByCurrentThread() // 返回用于读取操作的锁。 ReentrantReadWriteLock.ReadLock readLock() // 返回用于写入操作的锁。 ReentrantReadWriteLock.WriteLock writeLock()
ReentrantReadWriteLock数据结构
ReentrantReadWriteLock的UML类图如下:
从中可以看出:
(01) ReentrantReadWriteLock实现了ReadWriteLock接口。ReadWriteLock是一个读写锁的接口,提供了"获取读锁的readLock()函数" 和 "获取写锁的writeLock()函数"。
(02) ReentrantReadWriteLock中包含:sync对象,读锁readerLock和写锁writerLock。读锁ReadLock和写锁WriteLock都实现了Lock接口。读锁ReadLock和写锁WriteLock中也都分别包含了"Sync对象",它们的Sync对象和ReentrantReadWriteLock的Sync对象 是一样的,就是通过sync,读锁和写锁实现了对同一个对象的访问。
(03) 和"ReentrantLock"一样,sync是Sync类型;而且,Sync也是一个继承于AQS的抽象类。Sync也包括"公平锁"FairSync和"非公平锁"NonfairSync。sync对象是"FairSync"和"NonfairSync"中的一个,默认是"NonfairSync"。
参考代码(基于JDK1.7.0_40)
ReentrantReadWriteLock的完整源码
AQS的完整源码
其中,共享锁源码相关的代码如下:
public static class ReadLock implements Lock, java.io.Serializable { private static final long serialVersionUID = -5992448646407690164L; // ReentrantReadWriteLock的AQS对象 private final Sync sync; protected ReadLock(ReentrantReadWriteLock lock) { sync = lock.sync; } // 获取“共享锁” public void lock() { sync.acquireShared(1); } // 如果线程是中断状态,则抛出一场,否则尝试获取共享锁。 public void lockInterruptibly() throws InterruptedException { sync.acquireSharedInterruptibly(1); } // 尝试获取“共享锁” public boolean tryLock() { return sync.tryReadLock(); } // 在指定时间内,尝试获取“共享锁” public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout)); } // 释放“共享锁” public void unlock() { sync.releaseShared(1); } // 新建条件 public Condition newCondition() { throw new UnsupportedOperationException(); } public String toString() { int r = sync.getReadLockCount(); return super.toString() + "[Read locks = " + r + "]"; } }
说明:
ReadLock中的sync是一个Sync对象,Sync继承于AQS类,即Sync就是一个锁。ReentrantReadWriteLock中也有一个Sync对象,而且ReadLock中的sync和ReentrantReadWriteLock中的sync是对应关系。即ReentrantReadWriteLock和ReadLock共享同一个AQS对象,共享同一把锁。
ReentrantReadWriteLock中Sync的定义如下:
final Sync sync;
下面,分别从“获取共享锁”和“释放共享锁”两个方面对共享锁进行说明。
获取共享锁
获取共享锁的思想(即lock函数的步骤),是先通过tryAcquireShared()尝试获取共享锁。尝试成功的话,则直接返回;尝试失败的话,则通过doAcquireShared()不断的循环并尝试获取锁,若有需要,则阻塞等待。doAcquireShared()在循环中每次尝试获取锁时,都是通过tryAcquireShared()来进行尝试的。下面看看“获取共享锁”的详细流程。
1. lock()
lock()在ReadLock中,源码如下:
public void lock() { sync.acquireShared(1); }
2. acquireShared()
Sync继承于AQS,acquireShared()定义在AQS中。源码如下:
public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) doAcquireShared(arg); }
说明:acquireShared()首先会通过tryAcquireShared()来尝试获取锁。
尝试成功的话,则不再做任何动作(因为已经成功获取到锁了)。
尝试失败的话,则通过doAcquireShared()来获取锁。doAcquireShared()会获取到锁了才返回。
3. tryAcquireShared()
tryAcquireShared()定义在ReentrantReadWriteLock.java的Sync中,源码如下:
protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); // 获取“锁”的状态 int c = getState(); // 如果“锁”是“互斥锁”,并且获取锁的线程不是current线程;则返回-1。 if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; // 获取“读取锁”的共享计数 int r = sharedCount(c); // 如果“不需要阻塞等待”,并且“读取锁”的共享计数小于MAX_COUNT; // 则通过CAS函数更新“锁的状态”,将“读取锁”的共享计数+1。 if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) { // 第1次获取“读取锁”。 if (r == 0) { firstReader = current; firstReaderHoldCount = 1; // 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程 } else if (firstReader == current) { firstReaderHoldCount++; } else { // HoldCounter是用来统计该线程获取“读取锁”的次数。 HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != current.getId()) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); // 将该线程获取“读取锁”的次数+1。 rh.count++; } return 1; } return fullTryAcquireShared(current); }
说明:tryAcquireShared()的作用是尝试获取“共享锁”。
如果在尝试获取锁时,“不需要阻塞等待”并且“读取锁的共享计数小于MAX_COUNT”,则直接通过CAS函数更新“读取锁的共享计数”,以及将“当前线程获取读取锁的次数+1”。
否则,通过fullTryAcquireShared()获取读取锁。
4. fullTryAcquireShared()
fullTryAcquireShared()在ReentrantReadWriteLock中定义,源码如下:
final int fullTryAcquireShared(Thread current) { HoldCounter rh = null; for (;;) { // 获取“锁”的状态 int c = getState(); // 如果“锁”是“互斥锁”,并且获取锁的线程不是current线程;则返回-1。 if (exclusiveCount(c) != 0) { if (getExclusiveOwnerThread() != current) return -1; // 如果“需要阻塞等待”。 // (01) 当“需要阻塞等待”的线程是第1个获取锁的线程的话,则继续往下执行。 // (02) 当“需要阻塞等待”的线程获取锁的次数=0时,则返回-1。 } else if (readerShouldBlock()) { // 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程 if (firstReader == current) { } else { if (rh == null) { rh = cachedHoldCounter; if (rh == null || rh.tid != current.getId()) { rh = readHolds.get(); if (rh.count == 0) readHolds.remove(); } } // 如果当前线程获取锁的计数=0,则返回-1。 if (rh.count == 0) return -1; } } // 如果“不需要阻塞等待”,则获取“读取锁”的共享统计数; // 如果共享统计数超过MAX_COUNT,则抛出异常。 if (sharedCount(c) == MAX_COUNT) throw new Error("Maximum lock count exceeded"); // 将线程获取“读取锁”的次数+1。 if (compareAndSetState(c, c + SHARED_UNIT)) { // 如果是第1次获取“读取锁”,则更新firstReader和firstReaderHoldCount。 if (sharedCount(c) == 0) { firstReader = current; firstReaderHoldCount = 1; // 如果想要获取锁的线程(current)是第1个获取锁(firstReader)的线程, // 则将firstReaderHoldCount+1。 } else if (firstReader == current) { firstReaderHoldCount++; } else { if (rh == null) rh = cachedHoldCounter; if (rh == null || rh.tid != current.getId()) rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); // 更新线程的获取“读取锁”的共享计数 rh.count++; cachedHoldCounter = rh; // cache for release } return 1; } } }
说明:fullTryAcquireShared()会根据“是否需要阻塞等待”,“读取锁的共享计数是否超过限制”等等进行处理。如果不需要阻塞等待,并且锁的共享计数没有超过限制,则通过CAS尝试获取锁,并返回1。
5. doAcquireShared()
doAcquireShared()定义在AQS函数中,源码如下:
private void doAcquireShared(int arg) { // addWaiter(Node.SHARED)的作用是,创建“当前线程”对应的节点,并将该线程添加到CLH队列中。 final Node node = addWaiter(Node.SHARED); boolean failed = true; try { boolean interrupted = false; for (;;) { // 获取“node”的前一节点 final Node p = node.predecessor(); // 如果“当前线程”是CLH队列的表头,则尝试获取共享锁。 if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC if (interrupted) selfInterrupt(); failed = false; return; } } // 如果“当前线程”不是CLH队列的表头,则通过shouldParkAfterFailedAcquire()判断是否需要等待, // 需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。若阻塞等待过程中,线程被中断过,则设置interrupted为true。 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
说明:doAcquireShared()的作用是获取共享锁。
它会首先创建线程对应的CLH队列的节点,然后将该节点添加到CLH队列中。CLH队列是管理获取锁的等待线程的队列。
如果“当前线程”是CLH队列的表头,则尝试获取共享锁;否则,则需要通过shouldParkAfterFailedAcquire()判断是否阻塞等待,需要的话,则通过parkAndCheckInterrupt()进行阻塞等待。
doAcquireShared()会通过for循环,不断的进行上面的操作;目的就是获取共享锁。需要注意的是:doAcquireShared()在每一次尝试获取锁时,是通过tryAcquireShared()来执行的!
shouldParkAfterFailedAcquire(), parkAndCheckInterrupt()等函数已经在“Java多线程系列--“JUC锁”03之 公平锁(一) ”中详细介绍过,这里就不再重复说明了。
释放共享锁
释放共享锁的思想,是先通过tryReleaseShared()尝试释放共享锁。尝试成功的话,则通过doReleaseShared()唤醒“其他等待获取共享锁的线程”,并返回true;否则的话,返回flase。
1. unlock()
public void unlock() { sync.releaseShared(1); }
说明:该函数实际上调用releaseShared(1)释放共享锁。
2. releaseShared()
releaseShared()在AQS中实现,源码如下:
public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
说明:releaseShared()的目的是让当前线程释放它所持有的共享锁。
它首先会通过tryReleaseShared()去尝试释放共享锁。尝试成功,则直接返回;尝试失败,则通过doReleaseShared()去释放共享锁。
3. tryReleaseShared()
tryReleaseShared()定义在ReentrantReadWriteLock中,源码如下:
protected final boolean tryReleaseShared(int unused) { // 获取当前线程,即释放共享锁的线程。 Thread current = Thread.currentThread(); // 如果想要释放锁的线程(current)是第1个获取锁(firstReader)的线程, // 并且“第1个获取锁的线程获取锁的次数”=1,则设置firstReader为null; // 否则,将“第1个获取锁的线程的获取次数”-1。 if (firstReader == current) { // assert firstReaderHoldCount > 0; if (firstReaderHoldCount == 1) firstReader = null; else firstReaderHoldCount--; // 获取rh对象,并更新“当前线程获取锁的信息”。 } else { HoldCounter rh = cachedHoldCounter; if (rh == null || rh.tid != current.getId()) rh = readHolds.get(); int count = rh.count; if (count <= 1) { readHolds.remove(); if (count <= 0) throw unmatchedUnlockException(); } --rh.count; } for (;;) { // 获取锁的状态 int c = getState(); // 将锁的获取次数-1。 int nextc = c - SHARED_UNIT; // 通过CAS更新锁的状态。 if (compareAndSetState(c, nextc)) return nextc == 0; } }
说明:tryReleaseShared()的作用是尝试释放共享锁。
4. doReleaseShared()
doReleaseShared()定义在AQS中,源码如下:
private void doReleaseShared() { for (;;) { // 获取CLH队列的头节点 Node h = head; // 如果头节点不为null,并且头节点不等于tail节点。 if (h != null && h != tail) { // 获取头节点对应的线程的状态 int ws = h.waitStatus; // 如果头节点对应的线程是SIGNAL状态,则意味着“头节点的下一个节点所对应的线程”需要被unpark唤醒。 if (ws == Node.SIGNAL) { // 设置“头节点对应的线程状态”为空状态。失败的话,则继续循环。 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; // 唤醒“头节点的下一个节点所对应的线程”。 unparkSuccessor(h); } // 如果头节点对应的线程是空状态,则设置“文件点对应的线程所拥有的共享锁”为其它线程获取锁的空状态。 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; // loop on failed CAS } // 如果头节点发生变化,则继续循环。否则,退出循环。 if (h == head) // loop if head changed break; } }
说明:doReleaseShared()会释放“共享锁”。它会从前往后的遍历CLH队列,依次“唤醒”然后“执行”队列中每个节点对应的线程;最终的目的是让这些线程释放它们所持有的锁。
公平共享锁和非公平共享锁
和互斥锁ReentrantLock一样,ReadLock也分为公平锁和非公平锁。
公平锁和非公平锁的区别,体现在判断是否需要阻塞的函数readerShouldBlock()是不同的。
公平锁的readerShouldBlock()的源码如下:
final boolean readerShouldBlock() { return hasQueuedPredecessors(); }
在公平共享锁中,如果在当前线程的前面有其他线程在等待获取共享锁,则返回true;否则,返回false。
非公平锁的readerShouldBlock()的源码如下:
final boolean readerShouldBlock() { return apparentlyFirstQueuedIsExclusive(); }
在非公平共享锁中,它会无视当前线程的前面是否有其他线程在等待获取共享锁。只要该非公平共享锁对应的线程不为null,则返回true。
ReentrantReadWriteLock示例
package lock.demo9; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; public class ReadWriteLockTest1 { public static void main(String[] args) { // 创建账户 MyCount myCount = new MyCount("4238920615242830", 10000); // 创建用户,并指定账户 User user = new User("Tommy", myCount); // 分别启动3个“读取账户金钱”的线程 和 3个“设置账户金钱”的线程 for (int i = 0; i < 3; i++) { user.getCash(); user.setCash((i + 1) * 1000); } } } class User { private String name; // 用户名 private MyCount myCount; // 所要操作的账户 private ReadWriteLock myLock; // 执行操作所需的锁对象 User(String name, MyCount myCount) { this.name = name; this.myCount = myCount; this.myLock = new ReentrantReadWriteLock(); } public void getCash() { new Thread() { public void run() { myLock.readLock().lock(); try { System.out.println(Thread.currentThread().getName() + " getCash start"); myCount.getCash(); Thread.sleep(1); System.out.println(Thread.currentThread().getName() + " getCash end"); } catch (InterruptedException e) { } finally { myLock.readLock().unlock(); } } }.start(); } public void setCash(final int cash) { new Thread() { public void run() { myLock.writeLock().lock(); try { System.out.println(Thread.currentThread().getName() + " setCash start"); myCount.setCash(cash); Thread.sleep(1); System.out.println(Thread.currentThread().getName() + " setCash end"); } catch (InterruptedException e) { } finally { myLock.writeLock().unlock(); } } }.start(); } } class MyCount { private String id; // 账号 private int cash; // 账户余额 MyCount(String id, int cash) { this.id = id; this.cash = cash; } public String getId() { return id; } public void setId(String id) { this.id = id; } public int getCash() { System.out.println(Thread.currentThread().getName() + " getCash cash=" + cash); return cash; } public void setCash(int cash) { System.out.println(Thread.currentThread().getName() + " setCash cash=" + cash); this.cash = cash; } }
运行结果:
Thread-0 getCash start Thread-2 getCash start Thread-0 getCash cash=10000 Thread-2 getCash cash=10000 Thread-0 getCash end Thread-2 getCash end Thread-1 setCash start Thread-1 setCash cash=1000 Thread-1 setCash end Thread-3 setCash start Thread-3 setCash cash=2000 Thread-3 setCash end Thread-4 getCash start Thread-4 getCash cash=2000 Thread-4 getCash end Thread-5 setCash start Thread-5 setCash cash=3000 Thread-5 setCash end
结果说明:
(01) 观察Thread0和Thread-2的运行结果,我们发现,Thread-0启动并获取到“读取锁”,在它还没运行完毕的时候,Thread-2也启动了并且也成功获取到“读取锁”。
因此,“读取锁”支持被多个线程同时获取。
(02) 观察Thread-1,Thread-3,Thread-5这三个“写入锁”的线程。只要“写入锁”被某线程获取,则该线程运行完毕了,才释放该锁。
因此,“写入锁”不支持被多个线程同时获取。