# ReentrantLock
Reentrantlock
是可重入锁,内部实现了公平锁,也实现了非公平锁。
其实之前讲解 AQS
中实现的自定义锁就是一个可重入非公平锁,只不过 ReentrantLock
内部实现了公平与非公平两种锁。
一个新线程调用 tryAcquire
可以直接抢锁,而不需要管阻塞队列中是否有其他线程等待拿锁。这就是非公平的。
ReentrantLock
的公平实现:
protected final boolean tryAcquire(int acquires) { | |
final Thread current = Thread.currentThread(); | |
int c = getState(); | |
if (c == 0) { | |
if (!hasQueuedPredecessors() && | |
compareAndSetState(0, acquires)) { | |
setExclusiveOwnerThread(current); | |
return true; | |
} | |
} | |
else if (current == getExclusiveOwnerThread()) { | |
int nextc = c + acquires; | |
if (nextc < 0) | |
throw new Error("Maximum lock count exceeded"); | |
setState(nextc); | |
return true; | |
} | |
return false; | |
} |
公平锁在 c==0
时加上了对阻塞队列中线程的检查,也就是 hasQueuedPredecessors()
方法
public final boolean hasQueuedPredecessors() { | |
Node t = tail; | |
Node h = head; | |
Node s; | |
return h != t && | |
((s = h.next) == null || s.thread != Thread.currentThread()); | |
} |
这个方法必须要适用于两种线程:
- 被唤醒的
head
的后继节点:此时在acquireQueued
函数中会先调用tryAcquire
再将节点设为头节点。所以此时该节点的线程调用hasQueuedPredecessors
返回false
,可以获得锁。 - 刚准备获取锁的新线程:这里的新指的是线程没有在阻塞队列。该线程在调用
hasQueuedPredecessors
时阻塞队列里面的节点会发生变化。如果h == t
,说明队列没有初始化或者没有阻塞节点,直接返回false
。能继续执行代码,说明h!=t
,按理来说h.next
是不会为null
的,这个问题其实是出现在初始化队列时发生的:
if (t == null) { // 第一次入队,没有 dummy node 的存在,需先创建它 | |
if (compareAndSetHead(new Node())) | |
// 执行下面语句时发生中断,导致 head!=tail 且 head.next=null | |
tail = head; | |
} |
此时说明有个没拿到锁的线程正在初始化队列,而对于新的线程来说,要保证公平,就不能拿锁。
读者可能疑惑这种情况,所有阻塞节点都出队了,假设最后一个拿到锁的线程为 t,那么
head
和tail
都指向 t 的节点。
该函数还有细节:也就是先读取 tail
再读取 head
。
Node t = tail; | |
Node h = head; |
还是和之前的初始化队列代码一起看:
如果先读取的是 head
,就有可能出现 head==null
而 tail != null
的情况。同时为了避免重排序, head
和 tail
都被 volatile
修饰。
我个人有个问题就是:如果照着 enq
初始化队列时会中断的思路,那么如果刚执行完 tail=head
然后发生中断,那么此时再有线程 s
执行 hhasQueuedPredecessors
,就会判断 h==t
从而使得函数返回 false
,这不就和本意相违背了吗,明明有个线程正在进行入队操作,凭什么抢了对方的锁?
我看网上大多数关于该函数的讲解都没有提到过,彷佛就是避而不谈,当然也可能是我学艺不精,如果读者有什么见解,劳烦发邮箱:laurensvfevaa@gmail.com
# ReentrantReadWriteLock
该锁提供了两种锁:读锁和写锁。是为了适用于多读少写的场景,因为读操作可以共享,实现逻辑为:
- 获取读锁:如果没有线程获取写锁,就可以获取读锁
- 获取写锁:既没有读锁,也没有其他线程获得写锁,才可以获取。
读写锁都是可重入的,和 ReentrantLock
一样,可以选择公平或非公平。
public class Cache { | |
static Map<String, Object> map = new HashMap<>(); | |
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); | |
static Lock r = rwl.readLock(); | |
static Lock w = rwl.writeLock(); | |
public static Object get(String key) { | |
r.lock(); | |
try { | |
return map.get(key); | |
} finally { | |
r.unlock(); | |
} | |
} | |
public static void put(String key, Object val) { | |
w.lock(); | |
try { | |
map.put(key, val); | |
} finally { | |
w.unlock(); | |
} | |
} | |
} |
# 内部结构
在重写 AQS
方法前,需要解决的是,用一个整型同时记录读状态和写状态,一个整型变量维护多种状态的情况,一定要使用 “按位切割使用” 这个变量,此处设计为高 16 位记录读,低 16 位记录写
写线程获取锁修改 state
,就是正常的 +1
,可以理解为低 16 位 + 1;而读线程获取锁修改 state
,是高 16 位 + 1,即为每次 s tate+=SHARED_UNIT
,SHARED_UNIT 是一个很大数( 1<<16+1
。
为了对之后的源码讲解减轻压力,先提前介绍几个函数
sharedCount(int c)
函数:标识占有读锁的线程数量exclusiveCount(int c)
函数:表示占有写锁的线程数量
# WriteLock
写锁是独占锁,所以需要重写的是 tryAcquire
(当然,我指的是 Sync 重写该函数)
protected final boolean tryAcquire(int acquires) { | |
Thread current = Thread.currentThread(); | |
int c = getState(); | |
int w = exclusiveCount(c); // 写锁 | |
if (c != 0) { // 存在读锁或者写锁,可能是自己重入锁 | |
// 存在读锁或者写锁被其他线程占了(w=0 而 c!=0 就表示读锁不为 0) | |
if (w == 0 || current != getExclusiveOwnerThread()) | |
return false; | |
if (w + exclusiveCount(acquires) > MAX_COUNT) | |
throw new Error("Maximum lock count exceeded"); | |
// 重入锁,不需要 CAS | |
setState(c + acquires); | |
return true; | |
} | |
if (writerShouldBlock() || | |
!compareAndSetState(c, c + acquires)) | |
return false; | |
setExclusiveOwnerThread(current); | |
return true; | |
} |
tryAcquire
的大致流程就是,先判断有没有读锁,再判断当前写锁的独占线程是不是自己,最后判断获取的锁线程数量会不会超过 MAX_COUNT
。
写锁的释放操作与 ReentrantLock
相似,每次释放减少写状态,当写状态为 0 时表示写锁被释放。
# ReadLock
AQS
除了独占模式,还有共享模式,需要重写 tryReleaseShared
和 tryAcquireShared
函数。而若干个线程的读操作就可以在共享模式中执行。
获取读锁从 Java5 到 Java6 变得复杂了许多,主要是增加了一些新功能,如 getReadHoldCount()
方法,作用是返回当前线程获取读锁的次数,这种状态只能保存在 ThreadLocal
中,由线程自身维护,使得读锁实现变得复杂。此处给出的代码来自《Java 并发编程的艺术》代码清单 5-18,它删减了部分代码,只保留了必要的部分。
protected final int tryAcquireShared(int unused) { | |
for(;;) { | |
int c = getState(); | |
int nextc = c + (1 << 16); | |
if(nextc < c) // 高 16 位溢出了 | |
throw new Error("Maximum lock count exceeded"); | |
// 存在写锁并且当前线程没拿到写锁 | |
if(exclusiveCount(c) != 0 && owner != Thread.currentThread()) | |
return -1; | |
// CAS 失败可能是其他线程也在拿读锁,再循环直到获得读锁 | |
if(compareAndSetState(c,nextc)) | |
return 1; | |
} | |
} |
读锁的释放,不仅要修改 state
,还要修改计数器,也就是 HoldCounter
,这个待会会讲解。
# 理解计数器
Sync
还有两个内部类: HoldCounter
和 ThreadLocalHoldCounter
。
# HoldCounter
// HoldCounter-- 计数器,主要与读锁配套使用 | |
static final class HoldCounter { | |
// 计数 | |
int count = 0; | |
// 获取当前线程的 TID 属性的值 | |
final long tid = getThreadId(Thread.currentThread()); | |
} |
count
表示某个读线程重入的次数, tid
表示该线程的 tid
字段的值,该字段可以唯一标识一个线程。
# ThreadLocalHoldCounter
// 本地线程计数器 | |
static final class ThreadLocalHoldCounter | |
extends ThreadLocal<HoldCounter> { | |
// 重写初始化方法,在没有进行 set 的情况下,获取的都是该 HoldCounter 值 | |
public HoldCounter initialValue() { | |
return new HoldCounter(); | |
} | |
} |
ThreadLocalHoldCounter
重写了 ThreadLocal
的 initialValue
方法,** ThreadLocal
类可以将线程与对象相关联。** 在没有进行 set
的情况下, get
到的均是 initialValue
方法里面生成的那个 HolderCounter
对象。
# 参考
https://blog.csdn.net/anlian523/article/details/106173860
https://www.cnblogs.com/leesf456/p/5383609.html
https://pdai.tech/md/java/thread/java-thread-x-lock-ReentrantLock.html
https://pdai.tech/md/java/thread/java-thread-x-lock-ReentrantReadWriteLock.html
https://blog.51cto.com/stefanxfy/5083396#ReentrantReadWriteLock_9
https://www.cmsblogs.com/article/1391297853107867648
《Java 并发编程的艺术》第 5 章