基于jdk1.8进行分析的。
ReadWriteLock也就是我们常说的是读写锁。解释过来就是:
- 当写操作时,其它线程无法读取或写入数据,
- 当读操作时,其它线程无法写数据,但却可以读取数据。
前面介绍的ReentrantLock是独占锁,这样的话,也就意味着在并发量比较大的情况下,还是效率比较低,因为操作中读/读、读/写、写/写都不能同时发生,从某种程度上降低了程序的执行效率。对ReentrantLock不了解的朋友,请移步这里。
读写锁(ReadWriteLock):分为读锁(ReadLock)和写锁(WriteLock)。多个读锁不互斥,读锁与写锁互斥,这是由jvm自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多线程同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。(此处大致列一下,看后期结论!!!)下面我们看一下ReadWriteLock接口的代码。
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
定义了两个接口规范,readLock和writeLock。通过源码我们看到ReadWriteLock有两个实现类,分别是:
- ReentrantReadWriteLock
- ReadWriteLockView
ReadWriteLockView类,我们看下源码:
final class ReadWriteLockView implements ReadWriteLock {
public Lock readLock() { return asReadLock(); }
public Lock writeLock() { return asWriteLock(); }
}
这个类是在StampedLock的一个内部类。
而ReentrantReadWriteLock则是ReadWriteLock的具体实现。说先整理一下关于关于该类使用的一些基本知识点。
1.重入方面其内部的WriteLock可以获取ReadLock,但是反过来ReadLock想要获得WriteLock则永远都不要想。
2.WriteLock可以降级为ReadLock,顺序是:先获得WriteLock再获得ReadLock,然后释放WriteLock,这时候线程将保持Readlock的持有。反过来ReadLock想要升级为WriteLock则不可能。
3.ReadLock可以被多个线程持有并且在作用时排斥任何的WriteLock,而WriteLock则是完全的互斥。这一特性最为重要,因为对于高读取频率而相对较低写入的数据结构,使用此类锁同步机制则可以提高并发量。
4.不管是ReadLock还是WriteLock都支持Interrupt,语义与ReentrantLock一致。
5.WriteLock支持Condition并且与ReentrantLock语义一致,而ReadLock则不能使用Condition,否则抛出UnsupportedOperationException异常。
6.此锁最多支持 65535 个递归写入锁和 65535 个读取锁。试图超出这些限制将导致锁方法抛出 Error。
然后我们看一个DEMO演示。
package demo;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* ReadWriteLock测试例程
* @ClassName: ReadWriteLockDemo
* @Description: TODO
* @author BurgessLee
* @date 2019年4月24日
*
*/
public class ReadWriteLockDemo {
private static ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = readWriteLock.readLock();
private static Lock writeLock = readWriteLock.writeLock();
private static Map<String,Object> map = new HashMap<String,Object>();
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
for(int i = 0; i < 10; i++) {
map.put("key"+i, i+1);
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//读读
new Thread(new Runnable() {
@Override
public void run() {
get(map,0);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
get(map,0);
}
}).start();
//读写 写读互斥
// new Thread(new Runnable() {
//
// @Override
// public void run() {
// get(map,0);
// }
// }).start();
// new Thread(new Runnable() {
//
// @Override
// public void run() {
// put(map,0);
// }
// }).start();
//写写互斥
// new Thread(new Runnable() {
//
// @Override
// public void run() {
// put(map,0);
// }
// }).start();
// new Thread(new Runnable() {
//
// @Override
// public void run() {
// put(map,0);
// }
// }).start();
}
public static Integer get(Map<String,Object> map, int i) {
System.out.println("get ..................");
Integer getValue = -1;
if(map.size() < 0) {
//pass
return getValue;
}else {
System.out.println(Thread.currentThread().getName() + "-上读锁");
readLock.lock();
getValue = (Integer) map.get("key" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// readLock.unlock();
System.out.println(Thread.currentThread().getName() + "-释放读锁");
}
return getValue;
}
public static Integer put(Map<String,Object> map, int i) {
System.out.println("put ..................");
Integer putValue = -1;
if(map.size() > 50) {
return putValue;
}else {
System.out.println(Thread.currentThread().getName() + "-上写锁");
writeLock.lock();
map.put("key"+i, i+1);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
putValue = i+1;
// writeLock.unlock();
System.out.println(Thread.currentThread().getName() + "-释放写锁");
}
return putValue;
}
}
上面代码中演示了读读锁不互斥,读写,写读,以及写写都是互斥的。过程的话可以执行粘贴到本地运行一下就知道了。可以看到,代码下半部分都是将释放锁的过程直接注释掉了。以上就是DEMO演示部分。源码分析明天继续。
本来应该昨天更新,但是临时出了一些状况,所以今天继续。No more words. Show code!
我们看一下关于ReadWriteLock的源码。上面已经粘贴过了,此处不在贴出。声明了两个方法分别是readLock和writeLock两个规范。该接口有两个实现分别是ReadWriteLockView和ReentrantReadWriteLock,前者是StampedLock的一个内部类,后者才是具体的实现类,所以我们看一下ReentrantReadWriteLock。
通过源码结构我们可以看出内部有以下几个内部类,分别是:
- Sync 继承自AQS,之前在分析ReentrantLock的时候也有类似的结构
- NonFairSync Sync的子类,非公平性质的
- FairSync Sync的子类,公平性之的
- ReadLock 读锁类,当我们读取数据的时候需要加读锁
- WriteLock 写锁类,当我们写数据的时候需要加写锁
下面我们看具体的源码。
构造方法
ReentrantReadWriteLock()
public ReentrantReadWriteLock() {
this(false);
}
默认构造方法,通过调用this()实现,我们继续看。
ReentrantReadWriteLock(boolean fair)
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
同样通过我们分析ReentrantLock的经验我们可以看出,同样在构造方法中可以指定一个布尔值,分别对应公平锁和非公平锁的实现,此处并且多了ReadLock和WriteLock 的实例化,并且构造参数中都传入了this。
内部类
Sync
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
/*
* 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;
//more code....
NonfairSync
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
}
FairSync
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
通过三个内部类,我们看的出来,后两个都是继承自第一个类也就是Sync,后两个代码都很简洁,也没有构造方法,所以默认调用的是父类的构造方法,也就是:
Sync() {
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
}
核心函数
通过DEMO中演示的代码我们可以看出,通过实例化ReentrantReadWriteLock类调用readLock和writeLock方法分别来获取读锁和写锁。那么我们看一下对应的源码实现。
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
可以分别返回的就是readerLock和writerLock,那么这两个东西不难猜测,一定是ReentrantReadWriteLock的成员属性。我们看一下声明。
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
通过源码注释部分的内容,可以看出来,都是通过自己内部类的实例,而在ReentrantReadWriteLock构造方法中,我们可以看到当实例化ReentrantReadWriteLock的实例的时候,对应的ReadLock和WriteLock也进行了实例化,并且传入了this。关于读锁和写锁,在上锁和释放锁的时候都是通过调用的是lock方法和unlock方法实现的,下面我们进入正题部分,逐个分析读锁和写锁的具体实现。
WriteLock
lock()
public void lock() {
sync.acquire(1);
}
可以看到是直接使用的是sync的acquire方法,传入参数是1,所以根据之前分析AQS的源码经验来看,此处也是需要考虑重入的情况。
具体实现中调用的是acquire方法,源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
此时arg=1,并且我们看到了传入参数Node.EXCLUSIVE,也就是此时是独占锁的实现。然后分别调用tryAcquire和acquireQueued以及addWriter方法。
//AQS未实现,希望子类继承实现,否则抛出异常
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
//加入到同步队列中
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
//自旋,再次尝试获取锁
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
上面代码中只有tryAcquire是AQS希望子类继承实现的,现在我们看一下具体实现。
//尝试获取锁的方法
protected final boolean tryAcquire(int acquires) {
//记录当前线程
Thread current = Thread.currentThread();
//记录当前状态
int c = getState();
//重入数量
int w = exclusiveCount(c);
// 当前state不为0
//如果写锁状态为0,说明读锁此时被占用返回false
//如果写锁状态不为0且写锁没有被当前线程持有返回false
if (c != 0) {
//重入次数为0,且独占锁线程不是当前线程
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//数据校验
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//考虑重入情况,此时当前线程持有独占锁,重入次数更新
setState(c + acquires);
return true;
}
//state为0,尝试直接cas
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//设置独占锁为当前线程
setExclusiveOwnerThread(current);
return true;
}
代码中注释内容已经很清晰了,此处不再赘述,我们看里面调用到的方法,比较关心的也就是writerShouldBlock()
在Sync中的代码中如下:
abstract boolean writerShouldBlock();
所以我们看子类NonfairSync实现。
final boolean writerShouldBlock() {
return false; // writers can always barge
}
没错,我们看到了直接返回false,注释内容也写的很清楚,写线程总是互斥的。下面我们来看释放锁的实现。
unlock()
public void unlock() {
sync.release(1);
}
调用sync的release实现的。
release(int arg)
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
参数此时是1,调用的是AQS中的release方法。主要用来释放锁,考虑重入情况。我们看一下源码中涉及调用到的其他方法。
//需要子类继承实现,否则抛出异常
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
我们看一下具体的tryRelease方法的实现。
protected final boolean tryRelease(int releases) {
//不是当前线程抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//是当前线程,考虑重入情况
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)//如果释放完,state为0,此时释放锁,并设置独占锁线程为null
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
//判断独占锁线程是不是当前线程
protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
下面我们来看一下ReadLock的lock和unlock方法的实现。
public void lock() {
sync.acquireShared(1);
}
可以看到是通过sync调用acquireShared方法实现的,也就意味着这把锁是共享锁的实现。下面我们看acquireShared源码实现。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
里面涉及到用到其他方法,我们继续往下看。
//需要子类继承实现,否则抛出异常。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
具体实现如下:
protected final int tryAcquireShared(int unused) {
//记录当前线程
Thread current = Thread.currentThread();
//当前线程状态
int c = getState();
//持有当前独占锁线程数目不为0,并且持有独占锁线程不是当前线程
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
//返回值
return -1;
int r = sharedCount(c);//获取持有共享锁数量
//如果公平策略没有要求阻塞且重入数没有到达最大值,则直接尝试CAS更新state
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;
}
//用来处理CAS没成功的情况,逻辑和上面的逻辑是类似的,就是加了无限循环
return fullTryAcquireShared(current);
}
真正处理动作是通过doAcquireShared(int arg),方法实现的。
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这段方法的执行逻辑和之前分析AQS类似,此处不再赘述。想具体了解,可以移步这里。
下面我们看具体释放锁的源码。
unlock()
public void unlock() {
sync.releaseShared(1);
}
通过调用releaseShared实现,我们继续往下看。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
上面整个方法是用来释放共享锁的。我们继续看调用到的方法。
//需要子类实现,否则抛出异常
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
//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)) {
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);
}
//执行释放共享锁具体实现动作
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
所以这么来看,当一个线程持有写锁,因为是独占锁,所以此时另外一个线程想持有该资源的写锁是不可以的,但是当获取读锁的时候,通过源码我们看到,如果当前持有写锁的线程,如果不是当前同一个线程,那么此时也是可以获取到的。同理对于读锁是一个共享锁,那么当另外一个线程想要获取该资源的读锁时,是可以获取的,但是写锁是不可以的,因为写锁获取前加了判断。所以最后总结起来就是:
- 读不可以写,源码中上来第一行判断就是,如果持有读锁,想要获取写锁,那么直接失败
- 读可以读
- 写不可以写
- 写可以读,但是当获取读锁不是当前线程,那么直接获取失败
下面还有一个问题就是关于锁升级以及降级的问题。
通过上面的结论来看,当一个线程获取读锁,那么可能有其他线程也在读,所以不能升级为写锁。如果当一个线程获取了写锁,那么该线程此时持有该资源的读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。
关于公平锁实现的读写锁的过程,此处不再赘述,有想要了解的朋友,可以自己去看一下,跟非公平锁实现起来只是有些细节稍微不同,其他过程都类似。
以上就是全部内容,如果有不对的地方还请指正。