深入理解AQS和ReentrantLock源码分析
回顾下之前说到的管程模型MESA。这个模型当中有一个同步等待队列,获取锁失败的线程都会放入到这队列当中;还有一些条件等待队列,获取到锁的线程因为条件比如wait等待后,会进入条件等待队列。 之前说了synchronized是jvm层面对管程的实现。现在来说下jdk层面如何实现管程。
深入理解AQS
既然在jvm层面实现了管程,为什么还要从jdk(java)层面实现呢?首先synchronized的加锁解锁都是自动的,如果像通过代码控制什么时候加锁,什么时候解锁比较困难,其次synchronized的重量级锁状态开销是比较重。基于以上两点需要从java层面实现管程。那么实现管程需要实现些什么呢? 按照MESA模型来说,得实现同步等待队列和条件等待队列,还有入队出队,加锁解锁,等待唤醒等等。那么这个抽象层就是AQS(AbstractQueuedSynchronizer)
AQS概述
什么是AQS java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列、条件队列、独占获取、共享获取等,而这些行为的抽象就是基于 AbstractQueuedSynchronizer(简称AQS)实现的,AQS是一个抽象同步框架,可以用来实现一个依赖状态的同步器。 JDK中提供的大多数的同步器如Lock, Latch, Barrier等,都是基于AQS框架来实现的 一般是通过一个内部类Sync继承 AQS 将同步器所有调用都映射到Sync对应的方法
AQS具备的特性: 阻塞等待队列、 共享/独占、 公平/非公平、 可重入 、允许中断 AQS内部维护属性volatile int state,表示资源的可用状态; State三种访问方式: getState() setState() compareAndSetState() AQS定义两种资源共享方式
- Exclusive-独占,只有一个线程能执行,如ReentrantLock
- Share-共享,多个线程可以同时执行,如Semaphore/CountDownLatch
AQS定义两种队列
- 同步等待队列: 主要用于维护获取锁失败时入队的线程
- 条件等待队列: 调用await()的时候会释放锁,然后线程会加入到条件队列,调用 signal()唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁
不同的自定义同步器竞争共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
- isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
- tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
同步等待队列 AQS当中的同步等待队列也称CLH队列,CLH队列是Craig、Landin、Hagersten三人发明 的一种基于双向链表数据结构的队列,是FIFO先进先出线程等待队列,Java中的CLH队列是原 CLH队列的一个变种,线程由原自旋机制改为阻塞机制。 AQS 依赖CLH同步队列来完成同步状态的管理:
- 当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造 成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程
- 当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
- 通过signal或signalAll将条件队列中的节点转移到同步队列。(由条件队列转化为同 步队列)
条件等待队列 AQS中条件队列是使用单向列表保存的,用nextWaiter来连接: 调用await方法阻塞线程; 当前线程存在于同步队列的头结点,调用await方法进行阻塞(从同步队列转化到条 件队列)
基于AQS自定义锁
public class MyLock extends AbstractQueuedSynchronizer {
@Override
protected boolean tryAcquire(int arg) {
if (compareAndSetState(0,1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
setExclusiveOwnerThread(null);
setState(0);
return true;
}
public void lock(){
acquire(1);
}
public void unlock(){
release(1);
}
public boolean tryLock(){
return tryAcquire(1);
}
public boolean isLocked() {
return isHeldExclusively();
}
}
测试
public class MyLockTest {
public static int sum;
public final static MyLock lock = new MyLock();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
lock.lock();
try {
for (int j = 0; j < 1000; j++) {
sum++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
}
Thread.sleep(2000);
System.out.println(sum);
}
}
如果锁能够生效那么结果就是3000 自定义类继承AbstractQueuedSynchronizer,加锁解锁逻辑都已经实现了,后面会对AbstractQueuedSynchronizer加锁解锁的源码尽心分析,只需要自己实现共享还是独占的代码就好。
ReentrantLock深入分析
ReentrantLock概述
ReentrantLock是一种基于AQS框架的应用实现,是JDK中的一种线程并发访问的同步手段,它的功能类似于synchronized是一种互斥锁,可以保证线程安全。相对于 synchronized, ReentrantLock具备如下特点:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
- 与 synchronized 一样,都支持可重入
顺便总结了几点synchronized和ReentrantLock的区别:
- synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;
- synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过 ReentrantLock#isLocked判断;
- synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;
- synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以 被中断的;
- 在发生异常时synchronized会自动释放锁,而ReentrantLock需要开发者在finally 块中显示释放锁;
- ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指定时长的获取,更加灵活;
- synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁,而ReentrantLock对于已经在等待的线程是先来的线程先获得锁;
ReentrantLock的使用
ReentrantLock实现了Lock接口,Lock接口定义了锁的规范
lock 和unlock 就是加锁解锁 tryLock:获取锁成功返回true,失败返回false,带有参数的tryLock,在参数的时间内获取锁成功返回true,失败返回false lockInterruptibly 锁中断 newCondition 条件对象
可重入
@Slf4j
public class ReentrantLockDemo2 {
public static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
log.debug("execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
log.debug("execute method2");
method3();
} finally {
lock.unlock();
}
}
public static void method3() {
lock.lock();
try {
log.debug("execute method3");
} finally {
lock.unlock();
}
}
}
方法1lock加锁,未释放锁的情况下,调用方法2,方法2的加锁是同一把锁,不会因为方法1没解锁而导致死锁。
可中断
多个线程做同样的事情,其中一个线程执行完了,不需要其他线程在等待执行了,可以通过中断机制,让其他等待线程抛中断异常,在catch中做一些收尾的工作。
@Slf4j
public class ReentrantLockDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread thread1 = new Thread(()->{
log.info("Thread1:启动");
try {
lock.lockInterruptibly();
try {
log.info("做些什么事情");
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
log.info("事情已做完,不需要在执行");
}
},"Thread1");
lock.lock();
try {
log.info("main线程获取锁,并且做完了事情");
thread1.start();
Thread.sleep(1000);
thread1.interrupt();
log.debug("线程thread1执行中断");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
锁超时
tryLock方法给获取锁增加时间,在规定时间内获取到锁放回true,获取不到返回false
@Slf4j
public class ReentrantLockOutTImeDemo {
public static void main(String[] args) {
ReentrantLock lock = new ReentrantLock();
Thread thread1 = new Thread(()->{
log.info("Thread1:启动");
try {
if (lock.tryLock(2, TimeUnit.SECONDS)){
log.info("Thread1获取锁成功,执行了业务逻辑");
} else {
log.info("Thread1获取锁失败");
}
} catch (InterruptedException e) {
log.info("Thread1获取锁失败,中断退出");
}
},"Thread1");
lock.lock();
try {
log.info("main线程获取锁,并且做完了事情");
thread1.start();
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
公平锁和非公平锁
默认构造参数获取到ReentrantLock 是非公平锁,通过参数true构建的ReentrantLock 是公平锁
条件等待
@Slf4j
public class ReentrantLockConditionTest {
private static final ReentrantLock lock = new ReentrantLock();
private static Condition condition1 = lock.newCondition();
private static Condition condition2 = lock.newCondition();
private static boolean b1 = false;
private static boolean b2 = false;
public void demo1() {
new Thread(() -> {
lock.lock();
try {
log.info("Thread1获取锁");
while (!b1){
log.info("Thread1条件等待唤醒");
try {
condition1.await();
} catch (InterruptedException e) {
log.info("Thread1条件等待中断退出");
return ;
}
}
log.info("Thread1条件等待唤醒成功,继续完成任务");
} finally {
lock.unlock();
}
}, "Thread1").start();
}
public void demo2() {
new Thread(() -> {
lock.lock();
try {
log.info("Thread2获取锁");
while (!b2){
log.info("Thread2条件等待唤醒");
try {
condition2.await();
} catch (InterruptedException e) {
log.info("Thread2条件等待中断退出");
}
}
log.info("Thread2条件等待唤醒成功,继续完成任务");
} finally {
lock.unlock();
}
}, "Thread2").start();
}
public void demo3() {
new Thread(() -> {
lock.lock();
try {
log.info("休眠3秒后唤醒demo1");
Thread.sleep(3000);
log.info("唤醒demo1");
b1 = true;
condition1.signal();
log.info("休眠5秒后唤醒demo2");
Thread.sleep(5000);
log.info("唤醒demo2");
b2 = true;
condition2.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "Thread3").start();
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockConditionTest test = new ReentrantLockConditionTest();
log.info("开始执行demo1");
test.demo1();
log.info("开始执行demo2");
test.demo2();
test.demo3();
}
}
线程1获取锁后,通过条件等待,会释放锁,接着线程2可以获取锁,同样线程2条件等待后释放锁,线程3可以获取锁,然后执行唤醒操作。
ReentrantLock 源码分析
关注点:
- ReentrantLock加锁解锁的逻辑
- 公平和非公平,可重入锁的实现
- 线程竞争锁失败入队阻塞逻辑和获取锁的线程释放锁唤醒阻塞线程竞争锁的逻辑实现 ( 设计 的精髓:并发场景下入队和出队操作)
先来ReentrantLock 看加锁逻辑 lock是一个抽象方法,有公平锁和非公平锁的实现 公平锁: 非公平锁: 比较公平锁和非公平锁,非公平锁多了CAS的操作。非公平锁中当线程进来首先执行CAS操作如果成功,就设置当前线程独占这把锁。失败执行acquire(1); ,公平锁一进来就执行acquire(1); 。这就是ReentrantLock 公平锁和非公平锁的区别。
然后来看acquire(1); 是来干嘛的。以非公平锁为例子。如果当前线程CAS失败,说明当前线程加锁失败,加锁失败执行acquire(1); acquire 是父类AbstractQueuedSynchronizer 的方法,首先会判断tryAcquire 方法。 tryAcquire 方法在公平(FairSync)和非公平(NonfairSync)中有不同的实现。 以非公平为例:
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 这个方法就是判断是否能够获取到锁(重入),如果成功返回true,则表示该线程获取到锁,后续代码不会执行,如果返回false,就是获取锁失败,接着判断acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 。这部分就是获取锁失败,线程入队阻塞的逻辑。开头说过AQS定义了同步队列。这个队列是一个双向链表。先来看看这双向链表的结构。 回到刚才说获取锁失败执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 来看下addWaiter(Node.EXCLUSIVE), 方法
private Node addWaiter(mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
整个addWaiter方法就是创建队列放入队列。当前线程入队后,还没有阻塞那么acquireQueued 方法就是用来阻塞的
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;
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
由于parkAndCheckInterrupt() 通过这个方法来阻塞线程,当持有锁的线程解锁后,会调用unpark的方法唤醒阻塞的线程,那么阻塞的线程就会从parkAndCheckInterrupt() 这里开始执行,接着自旋。 acquireQueued这个方法就是用来阻塞线程 selfInterrupt();这个方式重新补上中断标志位
总结上面的加锁逻辑: 调用lock,如果是对于公平锁和非公平锁lock有不同的实现,公平锁直接调用acquire方法,非公平锁CAS尝试获取锁失败后调用acquire。 acquire就是获取锁的逻辑:
- 首先进行!tryAcquire(arg)的判断。对于公平和非公平tryAcquire又有不同实现。
- 以非公平为例,tryAcquire调用了nonfairTryAcquire(用于获取锁和锁重入的判断,失败返回false)。
- 获取锁失败时执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg))。addWaiter方法就是用来创建链表和添加节点(为获取不到锁的线程添加到该链表中);acquireQueued是用来阻塞未获取到锁的线程
- selfInterrupt();补上中断标志位,在上述过程中可能会清除中断标志为。
最后看下解锁逻辑 解锁逻辑核心代码:release
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
|