CountDownLatch/CyclicBarrier/Semaphore 使用过吗?
CountDownLatch/CyclicBarrier/Semaphore 使用过吗?下面详细介绍用法:
一,(等待多线程完成的)CountDownLatch
背景;
- countDownLatch(同步援助)是在java1.5被引入,跟它一起被引入的工具类还有CyclicBarrier(同步援助)、Semaphore(计数信号量)、concurrentHashMap和BlockingQueue(阻塞队列)。
- 存在于java.util.cucurrent包下。
概念理解:
让一些线程阻塞,直到另外一些线程完成一系列操作后才被唤醒。
主要方法介绍
通过一个计数器来实现的,计数器的初始值是线程的数量。await方法会被阻塞,每当一个线程执行完毕后,countDown方法会让计数器的值-1,不会阻塞,当计数器的值为0时,表示其他线程都执行完毕后,调用await方法的线程会被唤醒,继续执行。
源码:
(1),countDownLatch类中只提供了一个构造器:
CountDownLatch(int count) 构造一个 CountDownLatch初始化与给定的数。
(2)类中有三个方法是最重要的:
//调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行 public void await() throws InterruptedException { }; //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行 public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; //将count值减1 public void countDown() { };
使用举例:
public static void CountDownLatchUsed() throws InterruptedException { CountDownLatch downLatch = new CountDownLatch(6); for (int i = 0; i < 6; i++) { new Thread(()->{ System.out.println(Thread.currentThread().getName()+"\t 下自习走人"); downLatch.countDown(); },String.valueOf(i)).start(); } downLatch.await(); System.out.println(Thread.currentThread().getName()+"自习室关门走人"); }
运行结果:
小结:CountDownLatch和CyclicBarrier区别:
- countDownLatch是一个计数器,线程完成一个记录一个,计数器递减,只能使用一次。
- CyclicBarrier的计数器更像一个阀门,需要所有线程都到达,然后继续执行,计数器从0开始递增,同时提供reset()方法初始化计数值,可以多次使用。
了解更多:https://blog.csdn.net/chenssy/article/details/49794141#commentBox
二,(同步屏障)CyclicBarrier
概念理解:
CyclicBarrier字面上就是可循环使用的屏障。当一组线程得到一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续工作。进入屏障通过await方法。
主要方法介绍:
CyclicBarrier的内部是使用重入锁ReentrantLock和Condition。它有两个构造函数:
- CyclicBarrier(int parties):创建一个新的 CyclicBarrier,它将在给定数量的线程处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。(直到到达屏障时才会去执行)
CyclicBarrier(int parties, Runnable barrierAction):创建一个新的 CyclicBarrier,它将在给定数量的线程处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。
说明:
(1)parties:拦截线程的数量:
(2)barrierAction:
为CyclicBarrier接收的Runnable命令,用于在线程到达屏障时,优先执行barrierAction ,用于处理更加复杂的业务场景。
源码:
private void breakBarrier() { generation.broken = true; count = parties; trip.signalAll(); }
public CyclicBarrier(int parties, Runnable barrierAction) { if (parties <= 0) throw new IllegalArgumentException(); this.parties = parties; this.count = parties; this.barrierCommand = barrierAction; }
在CyclicBarrier中最重要的方法莫过于await()方法,在所有参与者都已经在此 barrier 上调用 await 方法之前,将一直等待。如下:
public int await() throws InterruptedException, BrokenBarrierException { try { return dowait(false, 0L); } catch (TimeoutException toe) { throw new Error(toe); // cannot happen } }
await()方法内部调用dowait(boolean timed, long nanos)方法:
其实await()的处理逻辑还是比较简单的:如果该线程不是到达的最后一个线程,则他会一直处于等待状态,除非发生以下情况:
- 最后一个线程到达,即index == 0
- 超出了指定时间(超时等待)
- 其他的某个线程中断当前线程
- 其他的某个线程中断另一个等待的线程
- 其他的某个线程在等待barrier超时
- 其他的某个线程在此barrier调用reset()方法。reset()方法用于将屏障重置为初始状态。
应用场景:
CyclicBarrier试用与多线程结果合并的操作,用于多线程计算数据,最后合并计算结果的应用场景。比如我们需要统计多个Excel中的数据,然后等到一个总结果。我们可以通过多线程处理每一个Excel,执行完成后得到相应的结果,最后通过barrierAction来计算这些线程的计算结果,得到所有Excel的总和。
应用实例:
比如开会要等所有人到齐了,才会开会:
public class BlockQueueDemo { public static void main(String[] args) { CyclicBarrier cyclicBarrier = new CyclicBarrier(4, new Runnable() { @Override public void run() { System.out.println("所有人都到齐了吧,咱们开会。。。"); } }); for (int i = 1; i <= 4; i++) { final int tempInt = i; new Thread(() -> { try { System.out.println(Thread.currentThread().getName() + "\t" + tempInt + "已到位"); cyclicBarrier.await(); } catch (InterruptedException e) { e.printStackTrace(); } catch (BrokenBarrierException e) { e.printStackTrace(); } }, String.valueOf(i)).start(); } } }
运行结果:
了解更多:https://blog.csdn.net/chenssy/article/details/70160595
三,(控制并发线程数的)Semaphore
概念理解:
信号量Semaphore是一个控制访问多个共享资源的计数器,它本质上是一个“共享锁”。
理论知识:
(1) Java并发提供了两种加锁模式,共享锁和独占锁。对于独占锁,他每次只能有一个线程持有,而共享锁则不同,它允许多个线程并行持有锁,并发的访问共享资源。
(2)独占锁采用的是一种悲观的加锁策略,对于写而言为冲突是必须的,但对于读而言可以进行共享,因为它不印象数据的一致性,如果某个只读线程获取独占锁,则其他读线程都只能等待了,这种情况下就限制了不必要的并发性,降低了吞吐量。而共享锁则不同,它放宽了加锁的条件,采用了乐观锁机制,它是允许多个读线程同时访问同一个共享资源的。
Semaphore官方解释:
一个计数信号量。从概念上讲,信号量维护了一个许可集。如有必要,在许可可用前会阻塞每一个 acquire(),然后再获取该许可。每个 release() 添加一个许可,从而可能释放一个正在阻塞的获取者。但是,不使用实际的许可对象,Semaphore 只对可用许可的号码进行计数,并采取相应的行动。
(3)Semaphore 通常用于限制可以访问某些资源(物理或逻辑的)的线程数目。
CountDownLatch和Semephore优缺点对比
CountDownLatch
的问题是不能复用。比如count=3
,那么加到3,就不能继续操作了。而Semaphore
可以解决这个问题,比如6辆车3个停车位,对于CountDownLatch
只能停3辆车,而Semaphore
可以停6辆车,车位空出来后,其它车可以占有,这就涉及到了Semaphore.accquire()
和Semaphore.release()
方法。
生活实例版本解释:
我们假设只有三个理发师、一个接待人。一开始来了五个客人,接待人则安排三个客人进行理发,其余两个人必须在那里等着,此后每个来理发店的人都必须等待。一段时间后,一个理发师完成理发后,接待人则安排另一个人(公平还是非公平机制呢??)来理发。在这里理发师则相当于公共资源,接待人则相当于信号量(Semaphore),客户相当于线程。
深入理解:
进一步讲,我们确定信号量Semaphore是一个非负整数(>=1)。当一个线程想要访问某个共享资源时,它必须要先获取Semaphore,当Semaphore >0时,获取该资源并使Semaphore – 1。如果Semaphore值 = 0,则表示全部的共享资源已经被其他线程全部占用,线程必须要等待其他线程释放资源。当线程释放资源时,Semaphore则+1;
当信号量Semaphore = 1 时,它可以当作互斥锁使用。其中0、1就相当于它的状态,当=1时表示其他线程可以获取,当=0时,排他,即其他线程必须要等待。
源码分析
(1)Semaphore的结构如下:
从上面可以看出,Semaphore和ReentrantLock一样,都是包含公平锁(FairySync)和非公平锁(NonfairSync),两个锁都是继承Sync,而Sync也是继承自AQS。其构造函数如下:
/** * 创建具有给定的许可数和非公平的公平设置的 Semaphore。 (默认是非公平锁) */ public Semaphore(int permits) { sync = new NonfairSync(permits); } /** * 创建具有给定的许可数和给定的公平设置的 Semaphore。 */ public Semaphore(int permits, boolean fair) { sync = fair ? new FairSync(permits) : new NonfairSync(permits); }
(2)信号量的获取:acquire()
在ReentrantLock中已经阐述过,公平锁和非公平锁获取锁机制的差别:对于公平锁而言,如果当前线程不在CLH队列(CLH锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性)的头部,则需要排队等候,而非公平锁则不同,它无论当前线程处于CLH队列的何处都会直接获取锁。所以公平信号量和非公平信号量的区别也一样。
关于CLH 自旋队列锁 更多知识:https://blog.csdn.net/aesop_wubo/article/details/7533186
源码:
public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); } public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) doAcquireSharedInterruptibly(arg); }
对于公平信号量和非公平信号量,他们机制的差异就体现在traAcquireShared()方法中:
公平锁 tryAcquireShared() 尝试获取信号量
源码:
protected int tryAcquireShared(int acquires) { for (;;) { //判断该线程是否位于CLH队列的列头,如果是的话返回 -1,调用doAcquireSharedInterruptibly() if (hasQueuedPredecessors()) return -1; //获取当前的信号量许可 int available = getState(); //设置“获得acquires个信号量许可之后,剩余的信号量许可数” int remaining = available - acquires; //如果剩余信号量 > 0 ,则设置“可获取的信号量”为remaining if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }
注意 : tryAcquireShared是尝试获取 信号量,remaining表示下次可获取的信号量。
(2)hasQueuedPredecessors() 是否有队列同步器
对于hasQueuedPredecessors、compareAndSetState在ReentrantLock中已经阐述了,hasQueuedPredecessors用于判断该线程是否位于CLH队列列头,compareAndSetState用于设置state的,它是进行原子操作的。代码如下:
public final boolean hasQueuedPredecessors() { Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); } protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }
(3) doAcquireSharedInterruptibly源代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { /* * 创建CLH队列的node节点,Node.SHARED表示该节点为共享锁 */ final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { //获取该节点的前继节点 final Node p = node.predecessor(); //当p为头节点时,基于公平锁机制,线程尝试获取锁 if (p == head) { //尝试获取锁 int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } //判断当前线程是否需要阻塞,如果阻塞的话,则一直处于阻塞状态知道获取共享锁为止 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } }
doAcquireSharedInterruptibly主要是做两个工作;1、尝试获取共享锁,2、阻塞线程直到线程获取共享锁。
非公平锁
对于非公平锁就简单多了,她没有那些所谓的要判断是不是CLH队列的列头,如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
final int nonfairTryAcquireShared(int acquires) { for (;;) { int available = getState(); int remaining = available - acquires; if (remaining < 0 || compareAndSetState(available, remaining)) return remaining; } }
在非公平锁中,tryAcquireShared直接调用AQS的nonfairTryAcquireShared()。通过上面的代码我可看到非公平锁并没有通过if (hasQueuedPredecessors())这样的条件来判断该节点是否为CLH队列的头节点,而是直接判断信号量。
信号量的释放:release()
信号量Semaphore的释放和获取不同,它没有分公平锁和非公平锁。如下:
public void release() { sync.releaseShared(1); } public final boolean releaseShared(int arg) { //尝试释放共享锁 if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; }
注意:release()释放线索所占有的共享锁,它首先通过tryReleaseShared尝试释放共享锁,如果成功直接返回,如果失败则调用doReleaseShared来释放共享锁。
tryReleaseShared:源码部分:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
protected final boolean tryReleaseShared(int releases) { for (;;) { int current = getState(); //信号量的许可数 = 当前信号许可数 + 待释放的信号许可数 int next = current + releases; if (next < current) // overflow throw new Error("Maximum permit count exceeded"); //设置可获取的信号许可数为next if (compareAndSetState(current, next)) return true; } }
doReleaseShared:源码部分:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
private void doReleaseShared() { for (;;) { //node 头节点 Node h = head; //h != null,且h != 尾节点 if (h != null && h != tail) { //获取h节点对应线程的状态 int ws = h.waitStatus; //若h节点状态为SIGNAL,表示h节点的下一个节点需要被唤醒 if (ws == Node.SIGNAL) { //设置h节点状态 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) continue; //唤醒h节点对应的下一个节点 unparkSuccessor(h); } //若h节点对应的状态== 0 ,则设置“文件点对应的线程所拥有的共享锁”为其它线程获取锁的空状态 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) continue; } //h == head时,则退出循环,若h节点发生改变时则循环继续 if (h == head) break; } }
多线程之Semaphore(信号量)应用
Semaphore是一个计数信号量,常用于限制可以访问某些资源(物理或逻辑的)线程数目。
常用函数:
信号量的构造函数
非公平:
public Semaphore(int permits);//permits就是允许同时运行的线程数目
公平(获得锁的顺序与线程启动顺序有关):
public Semaphore(int permits,boolean fair);//permits就是允许同时运行的线程数目
创建一个信号量
Semaphore semaphore = new Semaphore(2);
从信号量中获取一个许可
semaphore.acquire();
释放一个许可(在释放许可之前,必须先获获得许可。)
semaphore.release();
尝试获取一个许可,若获取成功返回true,若获取失败返回false
semaphore.tryAcquire();
所有函数:
// 创建具有给定的许可数和非公平的公平设置的 Semaphore。 Semaphore(int permits) // 创建具有给定的许可数和给定的公平设置的 Semaphore。 Semaphore(int permits, boolean fair) // 从此信号量获取一个许可,在提供一个许可前一直将线程阻塞,否则线程被中断。 void acquire() // 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞,或者线程已被中断。 void acquire(int permits) // 从此信号量中获取许可,在有可用的许可前将其阻塞。 void acquireUninterruptibly() // 从此信号量获取给定数目的许可,在提供这些许可前一直将线程阻塞。 void acquireUninterruptibly(int permits) // 返回此信号量中当前可用的许可数。 int availablePermits() // 获取并返回立即可用的所有许可。 int drainPermits() // 返回一个 collection,包含可能等待获取的线程。 protected Collection<Thread> getQueuedThreads() // 返回正在等待获取的线程的估计数目。 int getQueueLength() // 查询是否有线程正在等待获取。 boolean hasQueuedThreads() // 如果此信号量的公平设置为 true,则返回 true。 boolean isFair() // 根据指定的缩减量减小可用许可的数目。 protected void reducePermits(int reduction) // 释放一个许可,将其返回给信号量。 void release() // 释放给定数目的许可,将其返回到信号量。 void release(int permits) // 返回标识此信号量的字符串,以及信号量的状态。 String toString() // 仅在调用时此信号量存在一个可用许可,才从信号量获取许可。 boolean tryAcquire() // 仅在调用时此信号量中有给定数目的许可时,才从此信号量中获取这些许可。 boolean tryAcquire(int permits) // 如果在给定的等待时间内此信号量有可用的所有许可,并且当前线程未被中断,则从此信号量获取给定数目的许可。 boolean tryAcquire(int permits, long timeout, TimeUnit unit) // 如果在给定的等待时间内,此信号量有可用的许可并且当前线程未被中断,则从此信号量获取一个许可。 boolean tryAcquire(long timeout, TimeUnit unit)
代码实例:
假设有10个人在银行办理业务,只有2个工作窗口,代码实现逻辑如下
package com..concurrency.yuxin.demo; import lombok.extern.slf4j.Slf4j; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; @Slf4j public class SemaphoreDemo { // 排队总人数(请求总数) public static int clientTotal = 10; // 可同时受理业务的窗口数量(同时并发执行的线程数) public static int threadTotal = 2; public static void main(String[] args) throws Exception { ExecutorService executorService = Executors.newCachedThreadPool(); final Semaphore semaphore = new Semaphore(threadTotal); final CountDownLatch countDownLatch = new CountDownLatch(clientTotal); for (int i = 0; i < clientTotal; i++) { final int count = i; executorService.execute(() -> { try { semaphore.acquire(1); resolve(count); semaphore.release(1); } catch (Exception e) { log.error("exception", e); } countDownLatch.countDown(); }); } countDownLatch.await(); executorService.shutdown(); } private static void resolve(int i) throws InterruptedException { log.info("服务号{},受理业务中。。。", i); Thread.sleep(2000); } }
输出结果:
21:39:10.038 [pool-1-thread-1] INFO com.concurrency.yuxin.demo.SemaphoreDemo - 服务号0,受理业务中。。。
21:39:10.038 [pool-1-thread-2] INFO com.concurrency.yuxin.demo.SemaphoreDemo - 服务号1,受理业务中。。。
21:39:12.043 [pool-1-thread-4] INFO com.concurrency.yuxin.demo.SemaphoreDemo - 服务号3,受理业务中。。。
21:39:12.043 [pool-1-thread-3] INFO com.concurrency.yuxin.demo.SemaphoreDemo - 服务号2,受理业务中。。。
21:39:14.044 [pool-1-thread-6] INFO com.concurrency.yuxin.demo.SemaphoreDemo - 服务号5,受理业务中。。。
21:39:14.044 [pool-1-thread-5] INFO com.concurrency.yuxin.demo.SemaphoreDemo - 服务号4,受理业务中。。。
21:39:16.045 [pool-1-thread-7] INFO com.concurrency.yuxin.demo.SemaphoreDemo - 服务号6,受理业务中。。。
21:39:16.045 [pool-1-thread-8] INFO com.concurrency.yuxin.demo.SemaphoreDemo - 服务号7,受理业务中。。。
21:39:18.045 [pool-1-thread-10] INFO com.concurrency.yuxin.demo.SemaphoreDemo - 服务号9,受理业务中。。。
21:39:18.045 [pool-1-thread-9] INFO com.concurrency.yuxin.demo.SemaphoreDemo - 服务号8,受理业务中。。。
从输出结果可以看出,每隔两秒,有两个线程被执行
参考博客:
本文作者:试剑江湖
本文链接:https://www.cnblogs.com/iswxw/p/11663067.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 解答了困扰我五年的技术问题
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· 用 C# 插值字符串处理器写一个 sscanf
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· 为什么说在企业级应用开发中,后端往往是效率杀手?
· DeepSeek 解答了困扰我五年的技术问题。时代确实变了!
· 本地部署DeepSeek后,没有好看的交互界面怎么行!
· 趁着过年的时候手搓了一个低代码框架
· 推荐一个DeepSeek 大模型的免费 API 项目!兼容OpenAI接口!