【原创】Java并发编程系列15 | 重入锁ReentrantLock
2021-03-14 22:35
标签:queue 架构 可见性 new t 没有 控制 ++ 细节 monitor 点击上方“java进阶架构师”,选择右上角“置顶公众号” 本文为何适原创并发编程系列第 15 篇,文末有本系列文章汇总。 用法: 需要注意两点: 使用举例: 目的是得到test.inc=10000,但是因为线程安全问题,最终的结果总是小于10000。 ReentrantLock用内部类Sync来管理锁,所以真正的获取锁和释放锁是由Sync的实现类来控制的。 ReentrantLock分为公平锁和非公平锁,本文以公平锁为例讲解,下一篇将详细介绍公平锁与非公平锁。本文的源码讲解方式依然是在代码中适当位置加入注释。 四、释放锁 线程T获取到锁,AQS.state=1,AQS.exclusiveOwnerThread置为线程T。 state用于记录线程状态:state==0,没有线程占用该锁;state==1,一个线程持有该锁;state==n,一个线程持有该锁且重入了n次。 重入锁实现重入过程 重入锁实现同步过程: 《Java并发编程之美》 【原创】01|开篇获奖感言 你的“在看”,就是给我最好的赞赏^_^ 【原创】Java并发编程系列15 | 重入锁ReentrantLock 标签:queue 架构 可见性 new t 没有 控制 ++ 细节 monitor 原文地址:https://blog.51cto.com/15009303/2552798
收录于话题
#进阶架构师 | 并发编程专题
12个
20大进阶架构专题每日送达写在前面
AQS是java.util.concurrent包的核心基础组件,是实现Lock的基础。那么AQS是如何实现Lock的呢?
ReentrantLock是Lock中用到最多的,与synchronized具有相同的功能和内存语义,本文将从源码角度深入分析AQS是如何实现ReentrantLock的。
注:本文是在默认理解AQS原理基础上分析ReentrantLock的,建议读者先读懂上一篇AQS原理。1. ReentrantLock使用
public class ReentrantLockDemo {
private static ReentrantLock reentrantLock = new ReentrantLock();
public void createOrder() {
reentrantLock.lock();// 获取锁
try {
// 同步代码
} finally {
reentrantLock.unlock();// 释放锁
}
}
}
synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁。
为了保证在获取到锁之后,最终能够被释放,在finally块中释放锁。
synchronized文章中讲到的线程安全问题,代码如下:public class ReentrantLockTest {
public int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final ReentrantLockTest test = new ReentrantLockTest();
for (int i = 0; i 1)
// 保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
使用synchronized解决办法是,用synchronized修饰increase()方法。同样可以使用重入锁解决,代码如下:public class ReentrantLockTest {
private ReentrantLock reentrantLock = new ReentrantLock();
public int inc = 0;
public void increase() {
reentrantLock.lock();// 加锁
inc++;
reentrantLock.unlock();// 解锁
}
public static void main(String[] args) {
final ReentrantLockTest test = new ReentrantLockTest();
for (int i = 0; i 1)
// 保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
2. 类结构
public class ReentrantLock implements Lock, java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class FairSync extends Sync {}
static final class NonfairSync extends Sync {}
}
Sync有两个实现,分别为NonfairSync(非公平锁)和FairSync(公平锁),以FairSync为例来讲解ReentrantLock,之后会专门分析公平锁和非公平锁。3. 获取锁
/**
* 获取锁reentrantLock.lock()-->ReentrantLock.lock()
*/
public void lock() {
sync.lock();
}
/**
* ReentrantLock.lock()-->ReentrantLock.FairSync.lock()
*/
final void lock() {
acquire(1);
}
/**
* ReentrantLock.FairSync.lock()-->AbstractQueuedSynchronizer.acquire(int)
* 很熟悉了吧,上一篇讲的AQS获取锁的方法
* 1.当前线程通过tryAcquire()方法抢锁
* 2.线程抢到锁,tryAcquire()返回true,完成。
* 3.线程没有抢到锁,将当前线程封装成node加入同步队列,并将当前线程挂起,等待被唤醒之后再抢锁。
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
/**
* ReentrantLock.FairSync.tryAcquire(int)
* 实现了AQS的抢锁方法,抢锁成功返回true
* 获取锁成功的两种情况:
* 1.没有线程占用锁,且AQS队列中没有其他线程等锁,且CAS修改state成功。
* 2.锁已经被当前线程持有,直接重入。
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();// AQS的state (FairSync extends Sync extends AQS)
if (c == 0) {// state==0表示当前没有线程占用锁
if (!hasQueuedPredecessors() && // AQS同步队列中没有其他线程等锁的话,当前线程可以去抢锁,此方法下文有详解
compareAndSetState(0, acquires)) {// CAS修改state,修改成功表示获取到了锁
setExclusiveOwnerThread(current);// 抢锁成功将AQS.exclusiveOwnerThread置为当前线程
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
/*
* AQS.exclusiveOwnerThread是当前线程,表示锁已经被当前线程持有,这里是锁重入
* 重入一次将AQS.state加1
*/
int nextc = c + acquires;
if (nextc
/**
* 释放锁reentrantLock.unlock()-->ReentrantLock.unlock()
*/
public void unlock() {
sync.release(1);
}
/**
* ReentrantLock.unlock()-->AbstractQueuedSynchronizer.release(int)
* 同样是上一篇AQS中的释放锁方法
* 释放锁成功之后,唤醒head的后继节点next,next节点被唤醒后再去抢锁。
*/
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
/**
* AbstractQueuedSynchronizer.release(int)-->ReentrantLock.Sync.tryRelease(int)
* 释放重入锁。只有锁彻底释放,其他线程可以来竞争锁才返回true
* 锁可以重入,state记录锁的重入次数,所以state可以大于1
* 每执行一次tryRelease()将state减1,直到state==0,表示当前线程彻底把锁释放
*/
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;
}
5. 如何实现重入
线程T没释放锁之前再次调用lock()加锁,判断AQS.exclusiveOwnerThread==线程T,就可以直接执行不会阻塞,此时AQS.state加1。
此时线程T再次调用lock()加锁,继续重入,AQS.state再加1,此时state==2。
线程T执行完部分同步代码,调用unlock()解锁,AQS.state减1,此时state==1,线程T还持有该锁,其他线程还无法来竞争锁。
线程T执行完所有同步代码,调用unlock()解锁,AQS.state减1,此时state==0,线程将锁释放,允许其他线程来竞争锁。
总结
线程1调用lock()加锁,判断state=0,所以直接获取到锁,设置state=1 exclusiveOwnerThread=线程1。
线程2调用lock()加锁,判断state=1 exclusiveOwnerThread=线程1,锁已经被线程1持有,线程2被封装成节点Node加入同步队列中排队等锁。此时线程1执行同步代码,线程2阻塞等锁。
线程1调用unlock()解锁,判断exclusiveOwnerThread=线程1,可以解锁。设置state减1,exclusiveOwnerThread=null。state变为0时,唤醒AQS同步队列中head的后继节点,这里是线程2。
线程2被唤醒,再次去抢锁,成功之后执行同步代码。
线程最终获取到锁的标志就是AQS.state>0且AQS.exclusiveOwnerThread==当前线程。
Lock和AQS很好的隔离了使用者和实现者所需关注的领域。
参考资料
《Java并发编程实战》
《Java并发编程的艺术》并发系列文章汇总
【原创】02|并发编程三大核心问题
【原创】03|重排序-可见性和有序性问题根源
【原创】04|Java 内存模型详解
【原创】05|深入理解 volatile
【原创】06|你不知道的 final
【原创】07|synchronized 原理
【原创】08|synchronized 锁优化
【原创】09|基础干货
【原创】10|线程状态
【原创】11|线程调度
【原创】13|LockSupport
【原创】14|AQS源码分析
———— e n d ————
微服务、高并发、JVM调优、面试专栏等20大进阶架构师专题请关注公众号【Java进阶架构师】后在菜单栏查看。
回复【架构】领取架构师视频一套。
文章标题:【原创】Java并发编程系列15 | 重入锁ReentrantLock
文章链接:http://soscw.com/index.php/essay/64710.html