首先我们要知道怎么用这个锁,带着问题来进行源码分析
在接下来的代码中,我们创建了读写锁这个对象,并且从中获取了读锁和写锁,读写锁是互斥的,但是条件不一样,接下来我们就分析一下互斥条件;
1.同线程下,如果先获取读锁在获取写锁,下面这行代码中执行会成功打印读写锁2这几个字
2.同线程下,如果我们先获取读锁,在获取写锁,且读锁不进行解锁,那么线程将会阻塞
3.线程A,线程B A获取读锁,且不解锁,B获取写锁将被阻塞
4.线程A,线程B A获取写锁,且不解锁,B获取读锁将被阻塞
5.线程A,线程B A获取写锁,且不解锁,B获取写锁将可以成功被获取
结论:写锁在不同线程之间互斥,读锁在不同线程之间和写锁互斥,同线程之间 如果先来读锁后写锁,将会互斥,如果先写锁后读锁,将可以成功获取,并且不管读锁,还是写锁都是可重入锁
存在的疑问一: 读写锁互斥是如何实现的
存在的疑问二: 读写锁的内部实现
使用代码:接下来会通过这个使用代码来了解一下内部是如何实现的
//该行代码只是给出一个基本使用,可以自己拿java跑一下进行验证我上面的结论
//这一步选择是否是公平锁-
ReentrantReadWriteLock rlk = new ReentrantReadWriteLock(true);
//创建读锁
ReentrantReadWriteLock.ReadLock readLock = rlk.readLock();
//创建写锁
ReentrantReadWriteLock.WriteLock writeLock = rlk.writeLock();
new Thread(new Runnable() {
@Override
public void run() {
writeLock.lock();
readLock.lock();
System.out.println("读锁写锁2");
System.out.println(Thread.currentThread());
readLock.unlock();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
writeLock.lock();
readLock.lock();
System.out.println("读锁写锁1");
System.out.println(Thread.currentThread() /*线程名字,优先级,组名字*/);
}
}).start();
分析 ReentrantReadWriteLock 的架构
组织架构图:
classDiagram
AbstractQueuedSynchronizer<|-- Sync
Sync <|-- NofairSync
Sync <|-- fairSync
Lock <|--ReadLock
Lock <|--WriteLock
class ReentrantReadWriteLock{
-class Sync
-class FairSync
-class NonfairSync
-class ReadLock
-class WriteLock
}
分析类一:Sync
类的继承关系图
首先我们来一些前置知识,是来分析下面这段代码的,将过程写出来可能会更加直观的感受出来
-65535的Integer 类型的二进制数据是 11111111111111110000000000000001
65535的Integer 类型的二进制数据是 111111111111111100000000000000000
sharedCount = c >>> 16 右移16位放弃低符号位 并且符号不变
exclusiveCount = c&65535 与符号 那意味着这个只有正数 ,且小于2^8次幂
MAX_COUNT 最大数量 如果等于65536 那么共享的右移16位 如果一个那么还是0 如果是65536 那么最大就为1
EXCLUSIVE_MASK 这里是将符号作为与运算 也就是与上65535 低位与若1为1 不动高位 --
Sync 源码部分解析
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT); //65536
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; //65535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//65535
// 高16共享锁
static int sharedCount(int c) { return c >>> SHARED_SHIFT; } //无符号右移到低位 -65536
//低16位独享锁
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } //与 高位
/**
*一个用于每个线程读取的计数器。作为ThreadLocal维护;缓存在cachedHoldCounter
**/
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
//ThreadLocal子类。为了反序列化机制,最容易显式定义
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
//当前线程持有的可重入读锁的数量。仅在构造函数和readObject中初始化。当线程的读保持计数下降到0时移除
private transient ThreadLocalHoldCounter readHolds;
/**
成功获取readLock的最后一个线程的持有计数。在通常情况下,
下一个要释放的线程是最后一个要获取的线程,这样可以节省ThreadLocal查找。这是非易失性的,因为它只是作为一种启发
式使用,对于线程缓存来说非常好。 可以比正在缓存读保持计数
的线程存活更长时间,但通过不保留对线程的引用来避免垃圾保留。 通过良性数据竞争访问;依赖于内存模型的最终字段和out- thin-air保证
**/
private transient HoldCounter cachedHoldCounter;
// 第一个/或者说最后一个 将共享计数保持为1的计数
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
// 默认构造器,在初始化的时候,初始化,本地线程拥有技术,和status状态-
Sync() {
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
}
//读阻塞 写阻塞 子类实现
abstract boolean readerShouldBlock();
abstract boolean writerShouldBlock();
读锁-写锁-流程: ->
一: 初始化可重入读写类
//在读写锁里面
公平锁,直接乖乖排队,不管是写锁或者读锁
非公平锁, 写直接反感会false(不阻塞),读的话
判断是否有排它模式中等待的线程,如果没有的话直接返回false
public ReentrantReadWriteLock(boolean fair) {
//初始化锁是否为公平锁
sync = fair ? new FairSync() : new NonfairSync();
//初始化读锁-- 是否为公平锁的Sync类传入
readerLock = new ReadLock(this);
//初始化写锁
writerLock = new WriteLock(this);
}
//类似于这样
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
二: 获取读锁和写锁
//创建读锁
ReentrantReadWriteLock.ReadLock readLock = rlk.readLock();
//创建写锁
ReentrantReadWriteLock.WriteLock writeLock = rlk.writeLock();
获取读锁和写锁, 这个属于字段 在第一步的时候进行初始化了
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
三: 读锁上锁
获取读锁。 如果写锁没有被其他线程持有,则获取读锁并立即返回。 如果写锁由另一个线程持有,那么当前线程将出于线程调度的目的被禁用,并处于休眠状态,直到获得读锁
public void lock() {
sync.acquireShared(1);
}
//这个调用的是AQS的方法,然后又因为SYNC继承了该方法, 所以tryAcquireShared 会调用到共享锁里面,判断是否获取成功
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
/*
介绍:
1。如果写锁被另一个线程持有,则失败。
2. 否则,这个线程有资格获得锁wrt状态,所以询问它是否应该
因为队列策略而阻塞。如果没有,尝试通过套管状态和更新计数授
予。注意,step没有检查可重入获取,这被推迟到完整版本,以
避免在更典型的不可重入情况下必须检查hold count。
3.如果步骤2失败,因为线程显然不合格或CAS失败或计数饱和,则使用
完整的重试循环链接到版本。
*/
Thread current = Thread.currentThread();
int c = getState();
//如果其他线程拥有写锁,那么直接返回-1 阻塞
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 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);
}
}
四:写锁上锁
//这是写锁
public void lock() {
sync.acquire(1);
}
/**
独占模式下获取,忽略中断。通过至少调用一次tryAcquire来实现,
成功时返回。否则,线程将排队,可能重复阻塞和解除阻塞,调用
tryAcquire直到成功。这个方法可以用来实现Lock.lock方法。 参
数: 获取参数。这个值会传递给tryAcquire,但不会被解释,可以表
示你喜欢的任何东西。
**/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
介绍:
1.如果读计数非零或写计数非零且所有者是不同的线程,则失败。
2. 如果计数饱和,则失败。(这只会发生在count已经是非零的情况下。)
3.否则,如果该线程是可重入获取或队列策略允许,则该线程有资格获得锁。
如果是,更新state并设置owner。
**/
//相对读锁来说 写锁的尝试获取就吧暴力一点,我们可以认为只要满足判断条件 就可以获取写锁
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 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;
}
/**
以独占不中断模式获取已在队列中的线程。由条件等待方法和获取方法使用。
**/
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);
}
}