AQS简介及其常见实现

本文深入探讨了Java中ReentrantLock的使用方式及其实现原理,包括条件变量的应用、与synchronized的区别,同时还介绍了Semaphore、CountDownLatch及CyclicBarrier等并发工具类的功能与应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JAVA层面如何实现管程模型? synchronized、AQS两种实现

AQS

是什么

AQS 是一个抽象类AbstractQueuedSynchronizer,为同步器提供了通用的 执行框架。它定义了 资源获取和释放的通用流程,而具体的资源获取逻辑则由具体同步器通过重写模板方法来实现。 因此,可以将 AQS 看作是同步器的 基础“底座”,而同步器则是基于 AQS 实现的 具体“应用”

例如 可重入锁ReentrantLock)、信号量Semaphore)和 倒计时器CountDownLatch)。通过封装底层的线程同步机制,AQS 将复杂的线程管理逻辑隐藏起来,使开发者只需专注于具体的同步逻辑。

AQS是jdk并发包java.util.concurrent下绝大部分工具类实现的基础

AQS具备的特性:

  • 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取 锁和释放锁
    • getState - 获取 state 状态
    • setState - 设置 state 状态
    • compareAndSetState - cas 机制设置 state 状态
    • 独占模式是只有一个线程能够访问资源独占锁,如 ReentrantLock,而共享模式可以允许多个线程访问资源,如SemaphoreCountDownLatch
  • 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList 条件变量来实现等待、唤醒机制,只不过是JAVA实现的
  • 支持多个条件变量

具体实现

ReentrantLock

  • 具备AQS具备的特性, 这里重点讲一下条件变量:
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对于已经在等待的线程是先来的线程
先获得锁;

Semaphore

俗称信号量,它是操作系统中PV操作的原语在java的实现,它也是基于
AbstractQueuedSynchronizer实现的。经常用于限制获取资源的线程数量,或者限流器

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可以被重用。

使用场景

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能处理更为复杂的业务场景,比如如果计算发生错误,可
以重置计数器,并让线程们重新执行一次


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值