Lock锁机制原理(二)
在前面的文章中,我们已经详细地了解了Lock锁的底层实现原理,本篇文章将着眼于Lock锁的具体实现,讨论Java提供了哪些锁对象供开发者使用。
一、独占锁ReentrantLock
概念:ReentrantLock
是可重入的独占锁,同一时刻只能有一个线程拿到锁资源,其他会进入AQS
队列进行阻塞。
ReentrantLock
可以有两种模式:公平和非公平,这两个模式区别不大,只是在获锁的逻辑上稍有不同(一个先加入AQS
队列再根据情况尝试获锁;一个直接获锁,拿不到在加入队列)。
1)组成
ReentrantLock
类图如下:
跟我们在(一)中的自定义锁是不是类似~ ^ ^
可以看出,ReentrantLock
也是基于AQS
来实现的。
静态内部类Sync
:图中的辅助内部类Sync
直接继承于AQS
,为了实现公平和非公平,定义了它的两个子类FairSync
和NonfairSync
,这两个子类中只定义了一个tryAcquire()
方法。
state变量:AQS
中实现同步的关键字段state
的值表示线程获取该锁的可重入次数。如果state=0
,表明没有线程持有锁;否则表明锁被某个线程占有。
2)方法实现
Lock包下的锁获取和释放逻辑都是类似的,本质上都是对state
变量的修改。
获锁:当一个线程第一次获取RenntrantLock
锁时,会尝试使用CAS
将state
设置为1,如果设置成功表明成功拿到锁,记录该锁的持有者为当前线程;后续如果该线程再次尝试获取锁,state
值会增加表明重入的次数。
解锁:线程释放锁时会通过CAS
让state
值减少,当state
值减少到0时,当前线程会释放锁,并唤醒后续线程。
下面关注实现上述逻辑的几个关键方法:
由上一篇文章我们知道,实现
state
状态值修改的关键逻辑都在tryAcquire()
和tryRelease()
这两个方法中,因此下面我们以这个为基础展开讲解。
》获锁
-
tryAcquire(int acquires)
方法(Sync
类下定义,分为公平和非公平两种实现)非公平
//NonFairSync类下 protected final boolean tryAcquire(int acquires) { //调用基类的方法 return nonfairTryAcquire(acquires); }
//Sync基类下(默认非公平) final boolean nonfairTryAcquire(int acquires) { //获取当前线程和状态值 final Thread current = Thread.currentThread(); int c = getState(); //如果状态值为0,表明锁空闲,尝试获锁 if (c == 0) { //CAS获锁 if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } //如果当前线程是该锁持有者,执行重入逻辑 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires;/计算重入值 if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded");//重入达到上限溢出抛异常 setState(nextc); return true; } //拿不到锁;返回false return false; }
公平
//FairSync类下 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //公平只在这里多个hasQueuedPredecessors()方法 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; }
可以看到非公平下
tryAcquire()
方法只在获锁之前调用了hasQueuedPredecessors()
方法,我们看看这个方法做了什么public final boolean hasQueuedPredecessors() { Node h, s; //头节点不为null,表明AQS中有元素 if ((h = head) != null) { //如果第一个AQS的元素为空或者该元素取消了同步,找到合法节点 if ((s = h.next) == null || s.waitStatus > 0) { s = null; // traverse in case of concurrent cancellation for (Node p = tail; p != h && p != null; p = p.prev) {//从后往前遍历找到离头结点最近的合法节点,作为目标节点(可以尝试获锁的节点) if (p.waitStatus <= 0) s = p; } } //如果存在合法的目标节点并且不是当前线程节点,获锁失败,会阻塞在AQS中 if (s != null && s.thread != Thread.currentThread()) return true; } //head为空或者AQS的目标节点就是当前线程,返回false尝试获锁 return false; }
简单理解:公平锁会先加入
AQS
队列排队,只有排到自己才能去尝试获锁;非公平先直接尝试获锁,拿不到在加入队列。 -
lock
方法该方法是
ReentrantLock
锁对外提供的获锁方法,其本质还是调用了上面的tryAcquire()
方法,整个的调用链为lock()->acquire()->tryAcquire()
,其中acquire()
由AQS
实现,该方法会调用tryAcquire()
方法,而这个方法由子类去具体实现。(跟第一篇文章关联起来了,是不是很清晰)public void lock() { sync.acquire(1); }
//AQS类中 public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
上面的代码正是我们第一篇文章中分析过的方法,这里不在赘述。再次强调之前说过的结论:JUC
中任何锁的实现都会去自定义tryAcquire()
方法,该方法确切规定了锁对state
变量的控制,而对于具体的阻塞和唤醒操作,都依靠底层的AQS
类来实现!
其实上面两个方法已经包含了获锁的基本逻辑,但是ReentrantLock
还提供了几个方法供用户灵活使用,这几个方式实现了非阻塞式的获锁,避免发生死锁问题。
-
tryLock()
public boolean tryLock() { return sync.nonfairTryAcquire(1); }
该方法尝试获取锁,获取成功返回true;否则false。可以看出底层还是基于非公平获锁的方法来实现(主动尝试获取锁本身就是一种非公平行为)。
-
tryLock(long timeout, TimeUnit unit)
类似于上一个方法,但是加了超时时间,在一定时间内没拿到才返回false。
-
void lockInterruptibly()
与
lock
方法类似,但是会进行中断响应,底层还是使用了AQS
的acquireInterruptibly()
方法,这里不做过多介绍,有兴趣自行研究。
》解锁
解锁的操作与获锁类似,调用链为unlock()->release(int args)->tryRelease(int args)
,其中的关键还是由ReentrantLock
本身实现的tryRelease()
方法。
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {//上一篇文章的内容..
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
//辅助内部类Sync下
protected final boolean tryRelease(int releases) {
int c = getState() - releases;//更新state值
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();//当前线程没有锁肯定报异常
boolean free = false;//锁释放成功标志
if (c == 0) {//state等于0了就放锁
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;//返回成功标志
}
锁释放比较简单,不做过多介绍,强调一点:释放锁的本质还是对
state
变量做判断修改。
3)案例
最后来个简单的案例熟悉下ReentrantLock
的使用。我们用ReentrantLock
来实现一个线程安全的List
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockList {
private volatile ReentrantLock lock;
private List<Integer> list = new ArrayList<>();
public ReentrantLockList() {
this.lock = new ReentrantLock();
}
public void add(int num){
lock.lock();
try {
list.add(num);
}finally {
lock.unlock();
}
}
public void remove(int i){
lock.lock();
try {
list.remove(i);
}finally {
lock.unlock();
}
}
public int get(int i){
lock.lock();
try {
return list.get(i);
}finally {
lock.unlock();
}
}
@Override
public String toString() {
return "ReentrantLockList{" +
"list=" + list +
'}';
}
//模拟两个线程并发增加元素
public static void main(String[] args) throws InterruptedException {
ReentrantLockList lockList = new ReentrantLockList();
new Thread(() -> {
for(int i = 0; i < 10; ++i){
try {
Thread.sleep(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
lockList.add(i);
}
}).start();
new Thread(() -> {
for(int i = 10; i < 20; ++i){
try {
Thread.sleep(i-9);
} catch (InterruptedException e) {
e.printStackTrace();
}
lockList.add(i);
}
}).start();
Thread.sleep(1000);
System.out.println(lockList);
}
}
代码比较简单,第一个线程拿到锁后增加元素,此时第二个线程会加入AQS
队列进行阻塞,之后第一个线程增加完后唤醒阻塞的线程2,并睡眠,第二个线程拿到锁后也开始增加元素,后续是一样的。
二、读写锁ReentrantReadWriteLock
概念:上面提出的ReentrantLock
已经可以有效解决线程安全问题了,那么为什么还要提出ReentrantReadWriteLock
? 其实正如它的名字一样,ReentrantReadWriteLock
是为了解决在实际场景中的写少读多问题,该锁通过读写分离的策略,允许多个线程可以同时获取读锁,大大提高了多线程场景下读的性能。
1)组成
ReentrantReadWriteLock
类图(只展示出了关键方法):
大体结构与ReentrantLock
是类似的,但是ReentrantReadWriteLock
内部构造了两把锁:读锁ReadLock
和写锁WriteLock
,这两把锁依赖于Sync
来实现具体功能。
读写锁一个很巧妙地功能就是用一个state的高低位来表示读写两种的状态,这主要是因为CAS
只能对一个int类型的变量进行原子操作。我们来看看代码中是如何来定义的:
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
//偏移量
static final int SHARED_SHIFT = 16;
//共享锁状态单位值65536
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//共享锁线程最大数65535
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//排他锁掩码,低15位为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; }
static final class HoldCounter {
int count; // initially 0
// Use id, not reference, to avoid garbage retention
final long tid = LockSupport.getThreadId(Thread.currentThread());
}
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
//存放除第一个获取读锁线程外的其他线程获取读锁的可重入次数
private transient ThreadLocalHoldCounter readHolds;
//记录最后一个获取到读锁的线程获取到读锁的可重入次数
private transient HoldCounter cachedHoldCounter;
//记录第一个获取到读锁的线程
private transient Thread firstReader;
//第一个获取到读锁的线程获取读锁的可重入次数
private transient int firstReaderHoldCount;
Sync() {
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
}
/*
* 其他代码
**/
}
可以看出,ReentrantReadWriteLock
针对读锁提供了一些额外的类和字段来记录其状态;而因为写锁是排他的,所以只需要通过state变量的低16位来判断可重入数即可。
这里获取到读锁的线程的可重入状态是通过
ThreadLocal
来持有HoldCounter
对象来实现的(ThreadLocalHoldCounter
),ThreadLocal
是线程共享变量,可以存放线程独有的数据。
2)方法实现
这里以非公平锁为例
》获写锁
-
tryAcquire(int acquires)
方法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) {//读锁或写锁已被某线程获取 // (Note: if c != 0 and w == 0 then shared count != 0) if (w == 0 || current != getExclusiveOwnerThread()) return false;//如果有其他线程获取写锁,返回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; }
可以看出,这里通过state的值判断锁的获取状态,然后根据不同情况对锁状态进行更新。需要注意的是,这里
writerShouldBlock()
在公平锁和非公平锁下实现了不同的逻辑,简单来说就是在非公平条件下那么直接返回false,即直接去尝试修改state值;而公平条件下如果该写锁不是头节点,那么返回true,上面if条件直接成立,线程不会尝试修改state值,而是返回false。//FairSync公平实现 final boolean writerShouldBlock() { return hasQueuedPredecessors(); } //NoFairSync非公平实现 final boolean writerShouldBlock() { return false; // writers can always barge }
-
lock()
方法与
ReentrantLock
类似,ReentrantReadWriteLock
也提供了相关的接口供我们获锁和释放锁。其本质还是使用了Sync
提供的tryAcquire()/tryRelease()方法
public void lock() { sync.acquire(1); } public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
跟
ReentrantLock
完全相同,不多讲了。WriteLock
也提供了相应的非阻塞获读锁的方法,与ReentrantLock
是类似的,这里不做过多介绍。
》释放写锁
-
tryRelease()
方法protected final boolean tryRelease(int releases) { //判断是否是写锁持有者调用的解锁方法 if (!isHeldExclusively()) throw new IllegalMonitorStateException(); //获取可重入值(此时高16位一定为0) int nextc = getState() - releases; boolean free = exclusiveCount(nextc) == 0; //如果可重入值为0则释放锁 if (free) setExclusiveOwnerThread(null); //更新状态值 setState(nextc); return free; }
比较直观,根据锁的重入状态更新state值
-
unlock()
方法public void unlock() { sync.release(1); } public final boolean release(int arg) { //调用tryRelease方法尝试释放锁 if (tryRelease(arg)) { //释放成功(修改状态值成功),激活阻塞队列里面的一个线程 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
可以看出,
lock()
方法主要做了两件事:修改state值以及唤醒后续线程
》获取读锁
-
tryAcquireShared(int arg)
方法protected final int tryAcquireShared(int unused) { /* * Walkthrough: * 1. If write lock held by another thread, fail. * 2. Otherwise, this thread is eligible for * lock wrt state, so ask if it should block * because of queue policy. If not, try * to grant by CASing state and updating count. * Note that step does not check for reentrant * acquires, which is postponed to full version * to avoid having to check hold count in * the more typical non-reentrant case. * 3. If step 2 fails either because thread * apparently not eligible or CAS fails or count * saturated, chain to version with full retry loop. */ Thread current = Thread.currentThread(); int c = getState(); //判断写锁是否被占用(锁降级) if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current) return -1; //获取读锁计数 int r = sharedCount(c); //尝试获取读锁,成功则更新信息,否则(多个在获取)调用fullTryAcquireShared方法进行重试 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 != LockSupport.getThreadId(current)) cachedHoldCounter = rh = readHolds.get();//更新最后一个获取读锁的线程 else if (rh.count == 0)//如果是最后一个获取读锁的线程并且是第一次获取,记录在ThreadLocal结构中 readHolds.set(rh); rh.count++;//增加当前线程的读锁可重入数 } return 1; } return fullTryAcquireShared(current); }
上面的代码主要逻辑是获取state状态值判断写锁是否被占用,如果是,那么表明当前线程无法获取读锁,直接返回-1(阻塞);这里要注意如果是当前线程占用了写锁,那么依旧可以继续获取读锁(但没有释放写锁),这里可以进行锁降级操作。
还有一个要关注的点就是
readerShouldBlock()
方法,其非公平的实现如下: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(); }
该方法调用了
AQS
底层定义的apparentlyFirstQueuedIsExclusive()
final boolean apparentlyFirstQueuedIsExclusive() { Node h, s; return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null; }
这个方法会去判断
AQS
队列中的第一个阻塞线程是否在获取写锁,如果是,则让出竞争权,返回fullTryAcquireShared(current)
去自旋获取锁(如果写锁被拿了还是会阻塞)。这些操作的目的其实是为了防止获取读锁的线程长时间饥饿,所以也可以看成是一种写优先的思想。注:在公平条件下不会进行这个判断,还是直接进行排队,只有轮到自己才能去获取修改状态。
-
lock()
方法public void lock() { sync.acquireShared(1); } public final void acquireShared(int arg) { if (tryAcquireShared(arg) < 0) //类似于acquireQueued,提供共享模式下阻塞线程的方法 doAcquireShared(arg); }
熟悉的代码模式
ReadLock
也提供了相应的非阻塞获读锁的方法,与ReentrantLock
是类似的,这里不做过多介绍。
》释放读锁
-
tryreleaseShared(int arg)
方法protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread(); /* * 获取读锁的线程信息更新(第一个获取读锁的线程,最后一个获取读锁的线程,以及它们的可重入值..) **/ for (;;) {//循环直到自己读状态的计数值减一 int c = getState(); int nextc = c - SHARED_UNIT; if (compareAndSetState(c, nextc)) // 为0表明没有其他线程持有读锁(写锁),方法返回后会尝试唤醒后续阻塞线程(因为是共享锁,因此会进行传播唤醒) return nextc == 0; } }
这里的逻辑比较明确,但要注意一个问题,如果是发生了刚刚提出的锁降级操作,即当前线程把持住写锁,也获取读锁进行操作,即使后续读锁被释放,这里的
nextc==0
也是不成立的,只有读锁在写锁释放后释放才会满足这一条件。 -
unlock()
方法public void unlock() { sync.releaseShared(1); } public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { //传播唤醒后续线程 doReleaseShared(); return true; } return false; }
底层还是
tryReleaseShared()
方法。
3)案例
最后同样来个简单的案例理解一下ReentrantReadWriteLock
的使用。
import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantReadWriteLockList {
private ArrayList<Integer> arrayList = new ArrayList<>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();
private final Lock writeLock = lock.writeLock();
public void add(int num){
writeLock.lock();
try{
arrayList.add(num);
}finally {
writeLock.unlock();
}
}
public void remove(int index){
writeLock.lock();
try{
arrayList.remove(index);
}finally {
writeLock.unlock();
}
}
public void get(int index){
readLock.lock();
try{
arrayList.get(index);
}finally {
readLock.unlock();
}
}
}
上面的自定义List在修改元素时使用写锁,在访问元素时使用读锁,在读多写少的场景下性能会更好。
下面代码中的lockDown()
方法演示了锁降级的操作,这保证了数据的一致性。
public void lockDownShow(){
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
readLock.lock();
System.out.println(list.get(list.size() - 1));
readLock.unlock();
writeLock.lock();
try {
list.add(123456);
readLock.lock();//如果不先获取读锁,而是先释放写锁,那么就不是锁降级,因为其他线程可能会拿到写锁进行数据的修改
}finally {
writeLock.lock();
}
try {
System.out.println(list.get(list.size() - 1));
}finally {
readLock.unlock();
}
}
public static void main(String[] args) {
ReentrantReadWriteLockList list = new ReentrantReadWriteLockList();
list.lockDownShow();
}
结果:
如果没有锁降级,上面的123456可能是一个不确定的值(被其他线程修改)。
小结
本篇文章主要讲了lock
包下的两种锁实现ReentrantLock
和ReentrantReadWriteLock
,由于篇幅较长,将在后续文章中进一步探讨该包下一些线程同步器的实现原理。