基于AQS实现的Java并发工具类

AQS是AbstractQueuedSynchronizer的简称,类如其名,抽象的队列式的同步器,它是一个Java提高的底层同步工具类,用一个int类型的变量表示同步状态,并提供了一系列的CAS操作来管理这个同步状态,许多同步类实现都依赖于它,如常用的CountDownLatch、Semaphore、CyclicBarrier、ReentrantLock和StampedLock,后文会逐个介绍。

AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列),用于后续的调度。此外还可能有一个或者多个的Condition单向链表,用于Condition的处理,这个单向链表不是必须的,可能不存在。

AQS的资源共享方式
Exclusive(独占,只有一个线程能执行,如ReentrantLock)

Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)

state的作用
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证state能回到零状态。

更多ReentrantLock的讲解,请查看这篇博客

以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()调用主线程,然后调用主线程就会从await()函数返回,继续后续动作。

自定义同步器的方法
具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。

tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。

tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。

tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。

CountDownLatch
CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后等待的线程就可以恢复执行任务。

常见运用场景
多线程做资源初始化,主线程先暂停等待初始化结束;每个线程初始化结束后都countDown一次,等全部线程都初始化结束后(state=0),此时主线程再继续往下执行

示例代码:

 
@Slf4j    
public class CountDownLatchExample {    
    
    private final static int threadCount = 200;    
    
    public static void main(String[] args) throws Exception {    
    
        ExecutorService exec = Executors.newCachedThreadPool();    
        final CountDownLatch countDownLatch = new CountDownLatch(threadCount);    
//        Semaphore semaphore = new Semaphore(5);    
        for (int i = 0; i < threadCount; i++) {    
            final int threadNum = i;    
            exec.execute(() -> {    
                try {    
//                    semaphore.acquire(); // 获取一个许可    
                    test(threadNum);    
//                    semaphore.release(); // 释放一个许可    
                } catch (Exception e) {    
                    log.error("exception", e);    
                } finally {    
                    countDownLatch.countDown();    
                }    
            });    
        }    
        countDownLatch.await(10, TimeUnit.MILLISECONDS);    
        log.info("finish");    
        exec.shutdown();    
    }    
    
    private static void test(int threadNum) throws Exception {    
        Thread.sleep(100);    
        log.info("{}", threadNum);    
    }    
}
await可以设置时间限制,可以防止countdown没有全部进行导致的线程阻塞

调用await的线程只能是一个么?不是的,看下面的情况。

此时,AQS中,状态值state=2,对于 CountDownLatch 来说,state=2表示所有调用await方法的线程都应该阻塞,等到同一个latch被调用两次countDown后才能唤醒沉睡的线程。接着线程3和线程4执行了 await方法,这会的状态图如下:

注意,上面的通知状态是节点的属性,表示该节点出队后,必须唤醒其后续的节点线程。当线程1和线程2分别执行完latch.countDown方法后,会把state值置为0,此时,通过CAS成功置为0的那个线程将会同时承担起唤醒队列中第一个节点线程的任务,从上图可以看出,第一个节点即为线程3,当线程3恢复执行之后,其发现状态值为通知状态,所以会唤醒后续节点,即线程4节点,然后线程3继续做自己的事情,到这里,线程3和线程4都已经被唤醒,CountDownLatch功成身退。

上面的流程,如果落实到代码,把 state置为0的那个线程,会判断head指向节点的状态,如果为通知状态,则唤醒后续节点,即线程3节点,然后head指向线程3节点,head指向的旧节点会被删除掉。当线程3恢复执行后,发现自身为通知状态,又会把head指向线程4节点,然后删除自身节点,并唤醒线程4。

至于线程节点的状态设置的时机,其实是一个线程在阻塞之前,就会把它前面的节点设置为通知状态,这样便可以实现链式唤醒机制了。

Semaphore
Semaphore可以控制某个资源可被同时访问的个数,通过acquire()获取一个许可,如果没有就等待,而release() 释放一个许可。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。

示例代码见上文CountDownLatch的代码。

常见应用场景
Semaphore可以用来做流量控制,限制可以访问某些资源(物理或逻辑的),特别公用资源有限的应用场景,比如数据库连接。

Semaphore和RateLimiter的区别
Semaphore:作用是限定只有抢到信号的线程才能执行,其他的都得等待。你可以设置N个信号,这样最多可以有N个线程同时执行。注意,其他的线程只是挂起了,是通过限制线程个数来进行限流。

RateLimiter:Guava的限流工具类,基于令牌桶算法实现。作用是 限制一秒内只能有N个线程执行,超过了就只能等待下一秒。注意,N是double类型。是从速率来进行限流。

CyclicBarrier
CyclicBarrier可以使一定数量的线程反复地在栅栏位置处汇集。当线程到达栅栏位置时将调用await方法,这个方法将阻塞直到所有线程都到达栅栏位置。如果所有线程都到达栅栏位置,那么栅栏将打开,此时所有的线程都将被释放,而栅栏将会重置为原来的计数以便下次使用。

常见应用场景
用于多线程计算数据,最后合并计算结果的场景。每个parter负责一部分计算,最后进行数据汇总。

 
@Slf4j    
public class CyclicBarrierExample {    
    
    private static CyclicBarrier barrier = new CyclicBarrier(5, () -> {    
        log.info("callback is running"); // await触发后,先执行这个回调函数,例如这里可以做数据汇总    
    });    
    
    public static void main(String[] args) throws Exception {    
    
        ExecutorService executor = Executors.newCachedThreadPool();    
    
        for (int i = 0; i < 10; i++) {    
            final int threadNum = i;    
            Thread.sleep(1000);    
            executor.execute(() -> {    
                try {    
                    race(threadNum);    
                } catch (Exception e) {    
                    log.error("exception", e);    
                }    
            });    
        }    
        executor.shutdown();    
    }    
    
    private static void race(int threadNum) throws Exception {    
        Thread.sleep(1000);    
        log.info("{} is ready", threadNum);    
        // 例如这里可以做数据计算    
        barrier.await();    
        log.info("{} continue", threadNum);    
    }    
}
再来一段多次栅栏的代码。

 
class Solution {    
    public static void main(String[] args) {    
        ExecutorService executorService = Executors.newCachedThreadPool();    
        AtomicInteger count = new AtomicInteger(1);    
        CyclicBarrier cyclicBarrier = new CyclicBarrier(6,()->{    
            System.out.println(String.format("同学们到齐了+%d", count.getAndIncrement()));    
        });    
//        CyclicBarrier cyclicBarrier = new CyclicBarrier(6);    
        for (int i = 1; i <= 18; i++) {    
            executorService.execute(()-> {    
                System.out.println(Thread.currentThread().getName() + "开始等待其他线程");    
                    try {    
                        cyclicBarrier.await();    
                        System.out.println(Thread.currentThread().getName() + "开始执行业务逻辑,耗时0.5秒");    
                        // 工作线程开始处理,这里用Thread.sleep()来模拟业务处理    
                        Thread.sleep(500);    
                        System.out.println(Thread.currentThread().getName() + "业务逻辑执行完毕");    
                    } catch (InterruptedException e) {    
                        e.printStackTrace();    
                    } catch (BrokenBarrierException e) {    
                        e.printStackTrace();    
                    }    
                });    
        }    
        executorService.shutdown();    
    }    
}
与CountDownLatch的区别
1. 将count值递减的线程不同

在CountDownLatch中,执行countDown方法的线程和执行await方法的线程不是一类线程。例如,线程M,N需要等待线程A,B,C,D,E执行完成后才能继续往下执行,则线程A,B,C,D,E执行完成后都将调用countDown方法,使得最后count变为了0,最后一个将count值减为0的线程调用的tryReleaseShared方法会成功返回true,从而调用doReleaseShared()唤醒所有在sync queue中等待共享锁的线程,这里对应的就是M,N。所以,在CountDownLatch中,执行countDown的线程不会被挂起,调用await方法的线程会阻塞等待共享锁。
而在CyclicBarrier中,将count值递减的线程和执行await方法的线程是一类线程,它们在执行完递减count的操作后,如果count值不为0,则可能同时被挂起。例如,线程A,B,C,D,E需要互相等待,保证所有线程都执行完了之后才能一起通过。

2. 是否能重复使用

CountDownLatch是一次性的,当count值被减为0后,不会被重置。
而CyclicBarrier在线程通过栅栏后,会开启新的一代,count值会被重置。

3. 锁的类别与所使用到的队列

CountDownLatch使用的是共享锁,count值不为0时,线程在sync queue中等待,自始至终只牵涉到sync queue,由于使用共享锁,唤醒操作不必等待锁释放后再进行,唤醒操作很迅速。
CyclicBarrier使用的是独占锁,count值不为0时,线程进入condition queue中等待,当count值降为0后,将被signalAll()方法唤醒到sync queue中去,然后挨个去争锁(因为是独占锁),在前驱节点释放锁以后,才能继续唤醒后继节点。(不理解的话,好好看看上面CountDownlatch的两张图,理解了闭锁,这个就能理解了)

4. CyclicBarrier更强大

CyclicBarrier还提供了一些其他有用的方法,比如getNumberWaiting()方法可以获得CyclicBarrier阻塞的线程数量,isBroken()方法用来了解阻塞的线程是否被中断;

5. 运行方式的不同(与第一条解释类似)

CountDownLatch和CyclicBarrier都有让多个线程等待同步然后再开始下一步动作的意思,但是CountDownLatch的下一步的动作实施者是主线程,具有不可重复性;而CyclicBarrier的下一步动作实施者还是“其他线程”本身,具有往复多次实施动作的特点。

ReentrantLock
ReentrantLock是一个可重入且独占式的锁,它具有与使用synchronized监视器锁相同的基本行为和语义,但与synchronized关键字相比,它更灵活、更强大,增加了轮询、超时、中断等高级功能。ReentrantLock,顾名思义,它是支持可重入锁的锁,是一种递归无阻塞的同步机制。除此之外,该锁还支持获取锁时的公平和非公平选择。

想了解ReentrantLock和读写锁更多的信息,可以查看这篇文章

StampedLock
StampedLock是Java8引入的一种新的锁机制,它有三种模式(排它写,悲观读,乐观读),简单的理解,可以认为它是读写锁的一个改进版本,读写锁虽然分离了读和写的功能,使得读与读之间可以完全并发,但是读和写之间依然是冲突的,读锁会完全阻塞写锁,它使用的依然是悲观的锁策略。如果有大量的读线程,他也有可能引起写线程的饥饿。而StampedLock则提供了一种乐观的读策略,这种乐观策略的锁非常类似于无锁的操作,使得乐观锁完全不会阻塞写线程。当并发量大且读远大于写的情况下最快的的是StampedLock锁。建议大家采用。

下面是Oracle官方的代码示例:

 
class Point {    
        private double x, y;    
        private final StampedLock sl = new StampedLock();    
    
        // 排他写锁案例    
        void move(double deltaX, double deltaY) { // an exclusively locked method    
            long stamp = sl.writeLock();    
            try {    
                x += deltaX;    
                y += deltaY;    
            } finally {    
                sl.unlockWrite(stamp);    
            }    
        }    
    
        // 乐观读锁案例    
        double distanceFromOrigin() { // A read-only method    
            long stamp = sl.tryOptimisticRead(); // 获得一个乐观读锁    
            double currentX = x, currentY = y;  // 将两个字段读入本地局部变量    
            if (!sl.validate(stamp)) { //检查发出乐观读锁后同时是否有其他写锁发生?    
                stamp = sl.readLock();  // 如果没有,我们再次获得一个读悲观锁    
                try {    
                    currentX = x; // 将两个字段读入本地局部变量    
                    currentY = y; // 将两个字段读入本地局部变量    
                } finally {    
                    sl.unlockRead(stamp);    
                }    
            }    
            return Math.sqrt(currentX * currentX + currentY * currentY);    
        }    
    
        // 悲观读锁案例    
        void moveIfAtOrigin(double newX, double newY) { // upgrade    
            // Could instead start with optimistic, not read mode    
            long stamp = sl.readLock();    
            try {    
                while (x == 0.0 && y == 0.0) { // 循环,检查当前状态是否符合    
                    long ws = sl.tryConvertToWriteLock(stamp); //将读锁转为写锁    
                    if (ws != 0L) { // 这是确认转为写锁是否成功    
                        stamp = ws; // 如果成功 替换票据    
                        x = newX; // 进行状态改变    
                        y = newY;  // 进行状态改变    
                        break;    
                    } else { // 如果不能成功转换为写锁    
                        sl.unlockRead(stamp);  // 我们显式释放读锁    
                        stamp = sl.writeLock();  // 显式直接进行写锁 然后再通过循环再试    
                    }    
                }    
            } finally {    
                sl.unlock(stamp); // 释放读锁或写锁    
            }    
        }    
    }

————————————————
版权声明:本文为CSDN博主「全菜工程师小辉」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/y277an/article/details/95136569

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
AQS(AbstractQueuedSynchronizer)是Java并发编程中的一个重要,它可以理解为抽象的队列同步器。AQS提供了一种基于FIFO队列的同步机制,用于实现各种同步器,如ReentrantLock、CountDownLatch、Semaphore等。 AQS的核心思想是使用一个volatile的int型变量state来表示同步状态,通过CAS(Compare and Swap)操作来实现对state的原子更新。AQS内部维护了一个双向链表,用于保存等待获取同步状态的线程。 AQS的具体实现包括以下几个方面: 1. 内部属性:AQS内部有两个重要的属性,一个是head,表示队列的头节点;另一个是tail,表示队列的尾节点。 2. 入队操作:AQS的入队操作是通过enq方法实现的。在入队操作中,首先判断队列是否为空,如果为空,则需要初始化队列;否则,将新节点添加到队列的尾部,并更新tail指针。 3. CAS操作:AQS的CAS操作是通过compareAndSetHead和compareAndSetTail方法实现的。这些方法使用CAS操作来更新head和tail指针,保证操作的原子性。 4. 出队操作:AQS的出队操作是通过deq方法实现的。在出队操作中,首先判断队列是否为空,如果为空,则返回null;否则,将头节点出队,并更新head指针。 5. 同步状态的获取和释放:AQS提供了acquire和release方法来获取和释放同步状态。acquire方法用于获取同步状态,如果获取失败,则会将当前线程加入到等待队列中;release方法用于释放同步状态,并唤醒等待队列中的线程。 通过继承AQS,可以实现自定义的同步器。具体的实现方式是重写AQS的几个关键方法,如tryAcquire、tryRelease等,来实现对同步状态的获取和释放。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值