JAVA层面如何实现管程模型? synchronized、AQS两种实现
AQS
是什么
AQS是java.util.concurrent.locks包下的一个基础框架,用于构建锁和其他同步组件(如Semaphore、CountDownLatch等)。它简化了这些同步器的实现,提供了一个FIFO(先进先出)的等待队列,以及一个用于管理同步状态的原子性整数。可以把它理解为实现同步器的“脚手架”或“模板”。AQS 将复杂的线程管理逻辑隐藏起来,使开发者只需专注于具体的同步逻辑。AQS是jdk并发包java.util.concurrent下绝大部分工具类实现的基础
AQS具备的特性:
- state(同步状态) state 属性来表示资源的状态
-
它的具体含义由子类定义和实现。例如:
-
在
ReentrantLock中,state表示锁的持有计数(0表示未锁定,>0表示被锁定,且数值表示重入次数)。 -
在
Semaphore中,state表示剩余的许可证数量。 -
在
CountDownLatch中,state表示还需要等待的计数。
-
- FIFO 的等待队列,用于存放所有等待获取资源的线程
- 双向链表
- 类似于 Monitor 的 EntryList 条件变量来实现等待、唤醒机制,只不过是JAVA实现的
- 支持多个条件变量
-
AQS的两种资源共享模式
-
独占模式(Exclusive):资源一次只能被一个线程持有。例如:
ReentrantLock。 -
共享模式(Shared):资源可以被多个线程同时持有。例如:
Semaphore,CountDownLatch。
-
具体实现
ReentrantLock-可重入锁
-
模式:独占模式。
-
state的含义:表示锁的重入次数。当线程第一次获取锁时,
state变为1;如果该线程再次获取锁(重入),state会递增。释放锁时,state递减,直到为0才表示锁完全释放。 -
特点:支持公平锁和非公平锁(构造函数指定)。
-
公平锁:严格按照FIFO顺序从等待队列中获取锁。
-
非公平锁:新来的线程有机会“插队”直接尝试获取锁,可能比等待队列中的线程先拿到锁,吞吐量通常更高。
-
- 条件变量:
package com.test.jucdemo.lock;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import lombok.extern.slf4j.Slf4j;
/**
* @author
* 条件变量
*/
@Slf4j
public class ReentrantLockDemo6 {
private static ReentrantLock lock = new ReentrantLock();
private static Condition cigCon = lock.newCondition();
private static Condition takeCon = lock.newCondition();
private static boolean hashcig = false;
private static boolean hastakeout = false;
//送烟
public void cigratee(){
lock.lock();
try {
while(!hashcig){
try {
log.debug("没有烟,歇一会");
// 前半段: 释放锁,进入条件队列,然后阻塞线程
//过渡阶段: 被其他调用singal/singalAll的线程唤醒 (前提:要在同步队列中)
// 调用singal/singalAll的线程: 条件队列转同步队列,
// 可以在释放锁的时候唤醒head的后续节点所在的线程
// 后半段: 获取锁( 如果有竞争,cas获取锁失败,还会阻塞),
// 释放锁(唤醒同步队列中head的后续节点所在的线程)
// 后半段的逻辑其实就是独占锁的逻辑
cigCon.await();
}catch (Exception e){
e.printStackTrace();
}
}
log.debug("有烟了,干活");
}finally {
lock.unlock();
}
}
//送外卖
public void takeout(){
lock.lock();
try {
while(!hastakeout){
try {
log.debug("没有饭,歇一会");
takeCon.await();
}catch (Exception e){
e.printStackTrace();
}
}
log.debug("有饭了,干活");
}finally {
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockDemo6 test = new ReentrantLockDemo6();
new Thread(() ->{
test.cigratee();
}).start();
new Thread(() -> {
test.takeout();
}).start();
new Thread(() ->{
lock.lock();
try {
hashcig = true;
log.debug("唤醒送烟的等待线程");
cigCon.signal();
}finally {
lock.unlock();
}
},"t1").start();
new Thread(() ->{
lock.lock();
try {
hastakeout = true;
log.debug("唤醒送饭的等待线程");
takeCon.signal();
}finally {
lock.unlock();
}
},"t2").start();
}
}
- 几点synchronized和ReentrantLock的区别:
synchronized是JVM层次的锁实现,ReentrantLock是JDK层次的锁实现;
synchronized的锁状态是无法在代码中直接判断的,但是ReentrantLock可以通过
ReentrantLock#isLocked判断;
synchronized是非公平锁,ReentrantLock是可以是公平也可以是非公平的;
synchronized是不可以被中断的,而ReentrantLock#lockInterruptibly方法是可以
被中断的;
在发生异常时synchronized会自动释放锁,而ReentrantLock需要开发者在finally
块中显示释放锁;
ReentrantLock获取锁的形式有多种:如立即返回是否成功的tryLock(),以及等待指
定时长的获取,更加灵活;
synchronized在特定的情况下对于已经在等待的线程是后来的线程先获得锁(回顾
一下sychronized的唤醒策略),而ReentrantLock对于已经在等待的线程是先来的线程
先获得锁;
ReentrantReadWriteLock(可重入读写锁)
-
模式:内部有两把锁:读锁(共享)和写锁(独占)。
-
state的设计:一个
int类型的state被巧妙拆分成两部分:-
高16位:表示读锁的持有数量。
-
低16位:表示写锁的重入次数。
-
-
特点:
-
读写互斥:写锁被持有时,任何其他线程都无法获取读锁或写锁。
-
读读共享:多个线程可以同时持有读锁。
-
支持锁降级:一个线程在持有写锁的情况下,可以继续获取读锁,然后再释放写锁。这样就从写锁降级成了读锁。反之(锁升级)则不被允许。
-
Semaphore俗称信号量
-
模式:共享模式。
-
state的含义:表示可用许可证(permits)的数量。
-
工作方式:
-
acquire():获取一个许可证,state减1。如果无证可用,则线程阻塞。 -
release():释放一个许可证,state加1,并唤醒一个等待线程。
-
-
用途:用于控制同时访问特定资源的线程数量,如数据库连接池、流量控制。
package com.test.jucdemo.lock;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.Semaphore;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class SemaphoreTest2 {
/**
* 实现一个同时只能处理5个请求的限流器
*/
private static Semaphore semaphore = new Semaphore(5);
/**
* 定义一个线程池
*/
private static ThreadPoolExecutor executor = new ThreadPoolExecutor
(10, 50, 60,
TimeUnit.SECONDS, new LinkedBlockingDeque<>(200));
/**
* 模拟执行方法
*/
public static void exec() {
try {
//占用1个资源
semaphore.acquire(1);
//TODO 模拟业务执行
System.out.println("执行exec方法");
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
} finally {
//释放一个资源
semaphore.release(1);
}
}
public static void main(String[] args) throws InterruptedException {
{
for (; ; ) {
Thread.sleep(100);
// 模拟请求以10个/s的速度
executor.execute(() -> exec());
}
}
}
}
配合异常处理的样例代码
import java.util.concurrent.Semaphore;
public class SemaphoreInterruptExample {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(0); // 初始许可为0,表示没有可用许可
Thread workerThread = new Thread(() -> {
try {
System.out.println("Worker thread is trying to acquire a permit.");
semaphore.acquire(); // 尝试获取许可,这里会阻塞
System.out.println("Worker thread acquired a permit.");
} catch (InterruptedException e) {
System.out.println("Worker thread was interrupted while waiting for a permit.");
Thread.currentThread().interrupt(); // 恢复中断状态
} finally {
System.out.println("Worker thread is releasing the permit.");
semaphore.release(); // 释放许可
}
});
Thread interruptThread = new Thread(() -> {
try {
Thread.sleep(2000); // 等待2秒,让 workerThread 进入 acquire 的阻塞状态
System.out.println("Interrupt thread is interrupting worker thread.");
workerThread.interrupt(); // 中断 workerThread
} catch (InterruptedException e) {
e.printStackTrace();
}
});
workerThread.start();
interruptThread.start();
}
}
CountDownLatch
CountDownLatch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成
操作集。
使用场景
一般用作多线程倒计时计数器,强制它们等待其他一组(CountDownLatch的初始化决定)任务执行完成。CountDownLatch的两种使用场景:场景1:让多个线程等待、场景2:让单个线程等待。
-
让多个线程等待:模拟并发,让并发线程一起执行 (不常见)
package com.test.jucdemo.lock;
import java.util.concurrent.CountDownLatch;
/**
* 让多个线程等待:模拟并发,让并发线程一起执行
*/
public class CountDownLatchTest {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
//准备完毕……运动员都阻塞在这,等待号令
countDownLatch.await();
String parter = "【" + Thread.currentThread().getName() + "】";
System.out.println(parter + "开始执行……");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Thread.sleep(2000);// 裁判准备发令
countDownLatch.countDown();// 发令枪:执行发令
}
}
-
让单个线程等待:多个线程(任务)完成后,进行汇总合并(常见)
package com.test.jucdemo.lock;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
/**
* 让单个线程等待:多个线程(任务)完成后,进行汇总合并
*/
public class CountDownLatchTest2 {
public static void main(String[] args) throws Exception {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
final int index = i;
new Thread(() -> {
try {
Thread.sleep(1000 +
ThreadLocalRandom.current().nextInt(1000));
System.out.println(Thread.currentThread().getName()
+ " finish task" + index);
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
// 主线程在阻塞,当计数器==0,就唤醒主线程往下执行。
countDownLatch.await();
System.out.println("主线程:在所有任务运行完成后,进行结果汇总");
}
}
有没有可以改进的地方呢?
可以使用 CompletableFuture 类来改进!Java8 的 CompletableFuture 提供了很多对多线程友好的方法,使用它可以很方便地为我们编写多线程程序,什么异步、串行、并行或者等待所有线程执行完任务什么的都非常方便
package concurrent;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.thread.ThreadUtil;
import java.util.concurrent.CompletableFuture;
public class TestCompletableFuture {
public static void main(String[] args) {
// T1
CompletableFuture<Void> futureT1 = CompletableFuture.runAsync(() -> {
System.out.println("T1 is executing. Current time:" + DateUtil.now());
// 模拟耗时操作
ThreadUtil.sleep(1000);
});
// T2
CompletableFuture<Void> futureT2 = CompletableFuture.runAsync(() -> {
System.out.println("T2 is executing. Current time:" + DateUtil.now());
ThreadUtil.sleep(1000);
});
// 使用allOf()方法合并T1和T2的CompletableFuture,等待它们都完成
CompletableFuture<Void> bothCompleted = CompletableFuture.allOf(futureT1, futureT2);
// 当T1和T2都完成后,执行T3
bothCompleted.thenRunAsync(() -> System.out.println("T3 is executing after T1 and T2 have completed.Current time:" + DateUtil.now()));
// 等待所有任务完成,验证效果
ThreadUtil.sleep(3000);
}
}
上面的代码还可以继续优化,当任务过多的时候,把每一个 task 都列出来不太现实,可以考虑通过循环来添加任务。
//文件夹位置
List<String> filePaths = Arrays.asList(...)
// 异步处理所有文件
List<CompletableFuture<String>> fileFutures = filePaths.stream()
.map(filePath -> doSomeThing(filePath))
.collect(Collectors.toList());
// 将他们合并起来
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
fileFutures.toArray(new CompletableFuture[fileFutures.size()])
);
CyclicBarrier
-
注意:
CyclicBarrier的早期版本是基于ReentrantLock和Condition实现的,而非直接继承AQS,但它同样利用了AQS的思想。它是一个更高级的同步工具。 -
工作方式:让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障,所有被屏障拦截的线程才会继续运行。
-
与CountDownLatch的区别:
-
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以重置,能重复使用(Cyclic的含义)。 -
CyclicBarrier还支持一个可选的Runnable任务,在所有线程到达屏障后执行。
-
字面意思回环栅栏,通过它可以实现让一组线程等待至某个状态(屏障点)之后再全部同
时执行。回环是因为当所有等待线程都被释放以后,CyclicBarrier可以被重用。
使用场景
CyclicBarrier 可以用于多线程计算数据,最后合并计算结果的场景
package concurrent;
import java.util.Set;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 栅栏与闭锁 CountDownLatch 的关键区别在于,所有的线程必须同时到达栅栏位置,才能继续执行。
*/
public class CyclicBarrierTest2 {
//保存每个学生的平均成绩
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String,Integer>();
private ExecutorService threadPool= Executors.newFixedThreadPool(3);
// 提供一个 barrierAction,合并多线程计算结果
private CyclicBarrier cb=new CyclicBarrier(3,()->{
int result=0;
Set<String> set = map.keySet();
for(String s:set){
result+=map.get(s);
}
System.out.println("三人平均成绩为:"+(result/3)+"分");
});
public void count(){
for(int i=0;i<3;i++){
threadPool.execute(new Runnable(){
@Override
public void run() {
//获取学生平均成绩
int score=(int)(Math.random()*40+60);
map.put(Thread.currentThread().getName(), score);
System.out.println(Thread.currentThread().getName()
+"同学的平均成绩为:"+score);
try {
// 阻塞直到指定的线程都调用此方法,继续执行
//执行完运行await(),等待所有学生平均成绩都计算完毕
cb.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}
});
}
}
public static void main(String[] args) {
CyclicBarrierTest2 cb=new CyclicBarrierTest2();
System.out.println("======第一次调用======");
cb.count();
System.out.println("======第二次调用======");
cb.count();
}
}
- CyclicBarrier与CountDownLatch的区别:
CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()
方法重置。所以CyclicBarrier能处理更为复杂的业务场景,比如如果计算发生错误,可
以重置计数器,并让线程们重新执行一次
目录
ReentrantReadWriteLock(可重入读写锁)
让单个线程等待:多个线程(任务)完成后,进行汇总合并(常见)
CyclicBarrier还提供getNumberWaiting(可以获得CyclicBarrier阻塞的线程数量)、
isBroken(用来知道阻塞的线程是否被中断)等方法。
CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。
CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点
CountDownLatch一般用于一个或多个线程,等待其他线程执行完任务后,再执
CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执
CyclicBarrier 还可以提供一个 barrierAction,合并多线程计算结果。
CyclicBarrier是通过ReentrantLock的"独占锁"和Conditon来实现一组线程的阻塞
唤醒的,而CountDownLatch则是通过AQS的“共享锁”实现
参考链接
docs/java/concurrent/java-concurrent-questions-03.md · java小皮匠/JavaGuide - Gitee.com
AQS核心原理及常用实现
本文深入探讨了Java中ReentrantLock的使用方式及其实现原理,包括条件变量的应用、与synchronized的区别,同时还介绍了Semaphore、CountDownLatch及CyclicBarrier等并发工具类的功能与应用场景。
605

被折叠的 条评论
为什么被折叠?



