本文原载于 SegmentFault 社区专栏
作者:陈污龟
这篇文章讲的是 Java 的 Lock 锁,主要有以下知识点:
在学习 Lock 锁之前,我们先来看看什么是 AQS?
-
AQS 其实就是一个可以给我们实现锁的框架,juc 包中很多可阻塞的类比如 ReentrantLock、 ReadWriteLock 都是基于 AQS 构建的。
-
内部实现的关键是:先进先出的队列、state 状态
-
在 AQS 中实现了对等待队列的默认实现,子类只要重写部分的代码即可实现
(大量用到了模板代码)
-
AQS 同时提供了互斥模式
(exclusive)
和共享模式
(shared)
两种不同的同步逻辑。一般情况下,子类只需要根据需求实现其中一种模式,当然也有同时实现两种模式的同步类,如
ReadWriteLock
。
注意:ReentrantLock 不是 AQS 的子类,其内部类 Sync 才是 AQS 的子类。
State 状态
AQS 维护了一个
volatile int
类型的
state
变量,用来表示当前同步状态。
volatile 虽然不能保证操作的原子性,但是保证了当前变量 state 的可见性。
compareAndSetState
compareAndSetState 用来修改 state 状态,它是一个原子操作,底层其实是调用系统的 CAS 算法,有关 CAS 可移步:
CAS
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
请求资源
acquire
acquire(int arg) 以独占方式获取资源,如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
如果
tryAcquire(int)
方法返回 true,则 acquire 直接返回,否则当前线程需要进入队列进行排队。
addWaiter()
将该线程加入等待队列的尾部,并标记为独占模式。
学习 ReentrantLock 之前先来看看它实现的 Lock 接口
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
-
lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly() 是用来获取锁的。
-
-
newCondition() 方法是创建一个条件对象,用来管理那些得到锁但是不能做有用工作的线程。
ReentrantLock
,意思是"
可重入锁
",线程可以重复地获得已经持有的锁。ReentrantLock 是唯一实现了 Lock 接口的类。接下来我们来看看有关源码:
AQS子类
ReentrantLock 实现了三个内部类,分别是 Sync、NonfairSync 和FairSync。
abstract static class Sync extends AbstractQueuedSynchronizer
static final class NonfairSync extends Sync
static final class FairSync extends Sync
这些内部类都是 AQS 的子类,这就印证了我们之前所说的:AQS 是 ReentrantLock 的基础,AQS 是构建锁的框架.
构造器
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
默认实现的是非公平锁,传入 true 表示使用公平锁。
加锁
-
ReentrantLock 中加锁使用的是
lock
方法
-
加锁流程
首先会通过
CAS
方法,尝试将当前的 AQS 中的
State
字段改成从 0 改成 1,如果修改成功的话,说明原来的状态是 0,并没有线程占用锁,而且成功的获取了锁,只需要调用
setExclusiveOwnerThread
函将当前线程设置成持有锁的线程即可。否则,
CAS
操作失败之后,和普通锁一样,调用父类 AQS 的
acquire(1)
函数尝试获取锁。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
而在 AQS 的
acquire(1)
函数中,会判断
tryAcquire(1
)
以及
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
,如果尝试获取失败并且添加队列成功的话,那么就会调用
se
lfInt
errupt
函数中断线程执行,说明已经加入到了 AQS 的队列中。
注意:AQS 的
tryAcquire(1)
是由子类 Sync
(也就是 ReentrantLockd 的静态内部类)
自己实现的,也就是用到了模板方法,接下来我们去看看子类的实现。
tryAcquire
是在
NonfairSync
类中实现的,其中调用了
nonfairTryAcquire
函数。
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (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;
}
在 nonfairTryAcquire 函数中,会尝试让当前线程去获取锁:
-
-
如果当前 AQS 的状态为 0 的话,那么说明当前的锁没有被任何线程获取,则尝试做一次
CAS
操作,将当前的状态设置成
acquires
,如果设置成功了的话,那么则将当前线程设置成锁持有的线程,并且返回 true,表示获取成功。
-
如果当前的状态不为
0
的话,说明已经有线程持有锁,则判断当前线程与持有锁的线程是否相同,如果相同的话,则将当前的状态加上 acquires 重新将状态设置,并且返回 true,这也就是重入锁的原因。
-
如果当前线程没有获取到锁的话,那么就会返回 false,表示获取锁失败。
源码参考:ReentrantLock 中的 NonfairSync 加锁流程
https://www.jianshu.com/p/f7d05d06ef54
概述
我们知道 synchronized 内置锁和 ReentrantLock 都是
互斥锁
(一次只能有一个线程进入到临界区(被锁定的区域))
而 ReentrantReadWriteLock 是一个
读写锁
:
-
在读取数据的时候,可以多个线程同时进入到到临界区
(被锁定的区域)
-
一般来说:我们大多数都是读取数据得多,修改数据得少。所以这个读写锁在这种场景下就很有用了!
ReentrantReadWriteLock
实现了
ReadWriteLock
接口。
接口只有两个方法,一个用来获取读锁,一个用来获取写锁。也就是说将文件的读写操作分开,分成 2 个锁来分配给线程,从而使得多个线程可以同时进行读操作
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
性质
和 ReentrantLock 相比,ReentrantReadWriteLock 多了
ReadLock
和
WriteLock
两个内部类。
public class ReentrantReadWriteLock implements ReadWriteLock, java.io