1. ReentrantLock 使用方式
ReentrantLock.java
中给的 ReentrantLock 的使用方式(好多教程中推荐的写法估计也是从这来的吧)。
class X {
private final ReentrantLock lock = new ReentrantLock();
// ...
public void m() {
lock.lock(); // block until condition holds
try {
// ... method body
} finally {
lock.unlock()
}
}
}
lock.lock()
是加锁,lock.unlock()
锁释放,try{...}
是被加锁的业务逻辑。第三部分是开发者自己写的,那加锁、解锁内代码是怎么执行的呢?
2. 理下思路
在了解加锁、解锁内部代码之前,先来理下思路:
- 给线程加锁的目的是为了实现多线程同步;
- 多线程同步的意思是:当一个线程调用代码块 (方法中的一部分代码)时,在没有得到结果之间,这个调用不返回,同时其他线程也不能调用该代码块。
- 所以给代码块加锁,就是让多线程在该代码块上的并行变成串行,某一时刻只有能有一个线程在执行该代码块,剩余的其他都处于等待状态。
那处于等待状态的线程都在哪里等呢?当前线程怎么知道它是该执行还是该等呢?处于等待状态的线程什么时候(或者说怎么才能知道它要)执行代码块呢?
在 ReentrantLock 内部有一个同步器,它里面维护了个队列用于存放需要等待的线程,有个字段用于标记当前同步器中是否有线程正在执行代码块。当前线程是否需要等待,队列中的等待线如何调度,都由这个同步器负责,并保证某一时刻至多有一个线程在执行代码块。
3. 两个核心组件
3.1 ReentrantLock 里面都有什么。
public class ReentrantLock implements Lock, java.io.Serializable {
// 两个字段。
private static final long serialVersionUID = 7373984872572414699L;
private final Sync sync;
// 三个内部类。
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync{}
// 剩下的都是方法。
public void lock() { sync.lock();}
public void unlock() {sync.release(1);}
// ..... methods ..... //
}
serialVersionUID
是序列化用的,这里先不用管。
sync
就是上面提到的同步器,它有两个子类FairSync
和 FairSync
。公平锁和非公平锁就是从这出来的,具体的区别后面再讲。与其说是公平锁非公平锁,更确切的描述是共平同步器和非公平同步器。 看下 ReentrantLock 的构造函数:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平锁和非公平锁其实指的是 new ReentrantLock
时,里面的同步器是公平和还是非公平的。默认是非公平的,开发者也可以通过传递参数创建公平锁。
ReentrantLock 在构建对象时只创建了同步器,而且从代码里也可以看出,加锁、解锁(还有其他)的工作实际上都交给了同步器去执行。
3.2 AbstractQueuedSynchronizer 里面有什么
AbstractQueuedSynchronizer 就是常被念叨的 AQS,字面意思:抽象队列同步器,如果忘了 AQS 的大体作用,可以再瞥一眼第 2 部分。
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
// 个内部类
static final class Node {}
// 部分关键字段
private transient Thread exclusiveOwnerThread;
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
// 部分关键方法
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
}
队列是是由一个一个的 Node
(节点)作元素组成的,每一个线程在代码中的表现形式就是一个 Node。
static final class Node {
// 两个引用(指针)。
volatile Node next;
volatile Node prev;
// 线程。
volatile Thread thread;
// 线程的状态。
volatile int waitStatus;
// 还有其他的字段
}
链表中的节点由两部分构成,指针段和数据段。指针段负责表示当前节点的前驱结点和后继节点在哪里,数据段存放当前数据。thread
表示某一个等待的线程,waitStatus
表示该线程的等待状态。一个 thread 对应一个 node。
exclusiveOwnerThread
是 AQS 从父类 AbstractOwnableSynchronizer
继承来的,它表示当前正在执行代码块的线程。
head
指向头节点,tail
指向尾节点,当队列为空时,它俩的值都是 NULL。
state
表示同步状态,status == 0
表示当前时刻队列中没有线程在执行代码块,status == 1
表示当前时刻队列中有线程正在执行代码块。
compareAndSetState
专门用来更新 status 的值。
3.3 小结一下:
开发者调用 ReentrantLock 业务代码执行加锁、解锁,ReentrantLock 将具体的执行交给了 AbstractOwnableSynchronizer,而 AQS 内部维护着线程队列并进行调度,从而实现线程同步。此时得到了个大体的 AQS 框架,一个很 fu qian 的理解。
4. 一个完美的多线程并发。
当多线程并发执行的时候,会出现两种情况:
- 对加锁代码块有竞争,此时需要等待确保同步执行,花费稍微长一点时间。
- 对加锁代码块没竞争,运气特别好,在加锁代码块上恰好是一个线程执行完毕,另一个线程开始执行。这个是最完美的情况(虽说发生概率极小)。
看下在多线程没竞争的场景下,AQS 细节上是怎么执行的呢?
//套用开头的模板.
@Slf4j
public class SourceCode {
// 公平锁
private static ReentrantLock reentrantLock = new ReentrantLock(true);
public static void main(String[] args) {
reentrantLock.lock(); // 加锁
try {
log.debug("processing ..."); // 处理业务逻辑
} finally {
reentrantLock.unlock(); // 解锁
}
}
}
假设第一个线程 T1 执行到第六加锁:–> ReentrantLock.java
public void lock() {
//委托 sync 加锁
sync.lock();
}
–> ReentrantLock.FairSync.class
final void lock() {
acquire(1);
}
–> AbstractOwnableSynchronizer.class,实际上依旧是 ReentrantLock.FairSync.class。FairSync 继承了AbstractOwnableSynchronizer,所以 acquire(int arg) 方法也在 FairSync 里面。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
此时,arg == 1
先执行 tryAcquire(arg)
尝试请求锁(实际上是当前线程请求同步器,让自己执行代码块)。
–> ReentrantLock.FairSync.class
protected final boolean tryAcquire(int acquires) {
// 获取当前线程。
final Thread current = Thread.currentThread();
// 获取同步器状态。
int c = getState();
// c(state) == 0 表示没有线程正在执行代码块。
if (c == 0) {
if (!hasQueuedPredecessors() && // 判断队列中有没有前驱节点。
compareAndSetState(0, acquires)) { // 将 status 标记为 1。
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;
}
T1 怎么才能拿到锁呢(让同步器允许自己执行代码块)?3 个条件:
-
同步器的 state == 0 成立。T1 是第一个线程,state 当然等于 0。所以这个条件是成立的。
-
hasQueuedPredecessors == false
成立,T1 场景成不成立。// AbstractOwnableSynchronizer.java public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
new FairSync()
用的是无参构造函数,所以head
和tail
都是默认值 NULL,换个层面想,T1 是第一个线程,之前哪有线程入队呀。即h != t
是false
,&&
后面的代码就不执行了。所以!hasQueuedPredecessors()
是true
。 -
compareAndSetState(0, acquires)== true
成立,这是 cas 操作 T1 是第一个线程,所以执行肯定成功。
三个条件均满足,所以 T1 可以执行加锁代码块(拿锁成功),tryAcquire(int acquires)
返回 True
。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
(上面的代码挪下来)接下来,!tryAcquire(arg) == false
,if{...}
代码块不会执行, acquire(int arg)
返回。
final void lock() {
acquire(1);
}
lock()
返回。
@Slf4j
public class SourceCode {
// 公平锁
private static ReentrantLock reentrantLock = new ReentrantLock(true);
public static void main(String[] args) {
reentrantLock.lock(); // 加锁
try {
log.debug("processing ..."); // 处理业务逻辑
} finally {
reentrantLock.unlock(); // 解锁
}
}
}
reentrantLock.lock();
执行完毕,开始执行 try{...}
中的业务逻辑,这样 T1 就拿到了锁,可以执行加锁代码块。
当 T1 释放锁后,T2 紧接着开始拿锁。T2 依旧是重复了 T1 的过程,队列依旧是空,T3 、T4 也是同样的过程。这样看,多线程但交替执行,reentrantLock.lock();
只不过是多执行了一行代码,比 1.6 版本之前的 synchronized(obj){...}
在同样交替场景下的确是快不少。
4.1 两个小问题
拿锁的判断条件是 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)
问题一:hasQueuedPredecessors()
为什么要判断队列中是否有前驱节点?
上面流程的前提条件是并发但交替执行,但实际中大概率出现竞争。有个场景:T1 正在执行加锁代码块,T2、T3 在同步器的队列中等待,此时 T4 执行到 c = getstate()
恰好遇到 T1 释放锁了,T4 拿到 C == 0
。如果没有前驱判断,那 T4 就执行代码块了,但实际上应该要轮到 T2 执行,T4 入队等待。所以当线程拿到 c == 0
是还需要判断前驱节点由没有。
问题二:!hasQueuedPredecessors() && compareAndSetState()
, 为什么要在这 &&
?
假设当前同步器队列是空的,T2 、T3 挨的时间很近,几乎同时拿到 c == 0
,两线程都判断队列是空,现在就抢谁更快一些,先标记 state == 1
,慢的那个则 cas 执行失败,当然就进入等待。如果不将 cas 写在判断条件中,而是直接在 if 块中赋值,那么慢的线程即便 cas 失败,也同样能执行加锁代码块。
5. 公平锁和非公平锁的区别
前面提到过,公平锁和非公平锁内部实际上指的是公平同步器和非公平同步器。那这两个锁有什么区别呢?
他们之间的区别在两个地方:
第一处:lock()
方法中:
非公平锁在执行 lock()
时,先尝试让当前线程加一次锁。加锁失败后再走和公平锁一样的加锁流程。
// NonfairSync
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// FairSync
final void lock() {
acquire(1);
}
第二处:在执行 acquire(1)
方法中的 tryAcquire(int acquires)
时,
如果当前同步器的状态 state == 0
, 非公平锁会直接让线程加锁成功,而公平锁只有再确认同步器中没有前驱节点时,才会让线程加锁成功。
// NonfairSync
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 当 state == 0 时
if (c == 0) {
// 直接尝试让线程加锁
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;
}
return false;
}
// FairSync
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 当 statue == 0 时
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;
}
非公平锁看到state == 0
就要执行加锁,完全不顾及等待的线程,不讲武德。
如果是非公平锁,T1 正在执行,T2、T3在队列中等待,T4 如果执行到 compareAndSetState(0, 1) 前一瞬间恰好 T1 释放了锁,T4 直接加锁并执行代码块。