Java并发编程面试题:并发工具类(10题)
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,
15年
工作经验,精通Java编程
,高并发设计
,Springboot和微服务
,熟悉Linux
,ESXI虚拟化
以及云原生Docker和K8s
,热衷于探索科技的边界,并将理论知识转化为实际应用。保持对新技术的好奇心,乐于分享所学,希望通过我的实践经历和见解,启发他人的创新思维。在这里,我希望能与志同道合的朋友交流探讨,共同进步,一起在技术的世界里不断学习成长。
技术合作请加本人wx(注明来自csdn):foreast_sea
Java并发编程面试题:并发工具类(10题)
1. CountDownLatch(倒计数器)了解吗?
CountDownLatch
是 JUC 包中的一个同步工具类,用于协调多个线程之间的同步。它允许一个或多个线程等待,直到其他线程中执行的一组操作完成。它通过一个计数器来实现,该计数器由线程递减,直到到达零。
- 初始化:创建
CountDownLatch
对象时,指定计数器的初始值。 - 等待(
await
):一个或多个线程调用await
方法,进入等待状态,直到计数器的值变为零。 - 倒计数(
countDown
):其他线程在完成各自任务后调用countDown
方法,将计数器的值减一。当计数器的值减到零时,所有在 await 上等待的线程会被唤醒,继续执行。
当等待多个线程完成各自的启动任务后再启动主线程的任务,就可以使用 CountDownLatch
,以王者荣耀为例。
创建五个线程,分别代表大乔、兰陵王、安其拉、哪吒和铠等五个玩家。每个玩家都调用了countDown()
方法,表示已经就位。主线程调用await()
方法,等待所有玩家就位。
public static void main(String[] args) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(5);Thread daqiao = new Thread(() -> {System.out.println("大乔已就位!");countDownLatch.countDown();});Thread lanlingwang = new Thread(() -> {System.out.println("兰陵王已就位!");countDownLatch.countDown();});Thread anqila = new Thread(() -> {System.out.println("安其拉已就位!");countDownLatch.countDown();});Thread nezha = new Thread(() -> {System.out.println("哪吒已就位!");countDownLatch.countDown();});Thread kai = new Thread(() -> {System.out.println("铠已就位!");countDownLatch.countDown();});daqiao.start();lanlingwang.start();anqila.start();nezha.start();kai.start();countDownLatch.await();System.out.println("全员就位,开始游戏!");
}
再比如说,可以使用 CountDownLatch 确保某些操作在一组操作完成之后才开始执行。
五个玩家在等待倒计时结束后,一起出击。
private static void waitToFight(CountDownLatch countDownLatch, String name) {try {countDownLatch.await(); // 在此等待信号再继续System.out.println(name + " 收到,发起进攻!");} catch (InterruptedException e) {Thread.currentThread().interrupt();System.out.println(name + " 被中断");}
}public static void main(String[] args) {CountDownLatch countDownLatch = new CountDownLatch(1);Thread daqiao = new Thread(() -> waitToFight(countDownLatch, "大乔"), "Thread-大乔");Thread lanlingwang = new Thread(() -> waitToFight(countDownLatch, "兰陵王"), "Thread-兰陵王");Thread anqila = new Thread(() -> waitToFight(countDownLatch, "安琪拉"), "Thread-安琪拉");Thread nezha = new Thread(() -> waitToFight(countDownLatch, "哪吒"), "Thread-哪吒");Thread kai = new Thread(() -> waitToFight(countDownLatch, "凯"), "Thread-凯");daqiao.start();lanlingwang.start();anqila.start();nezha.start();kai.start();try {Thread.sleep(5000); // 模拟准备时间} catch (InterruptedException e) {Thread.currentThread().interrupt();System.out.println("主线程被中断");}System.out.println("敌军还有 5 秒到达战场,全军出击!");countDownLatch.countDown(); // 发出信号
}
CountDownLatch 的核心方法也不多:
CountDownLatch(int count)
:创建一个带有给定计数器的 CountDownLatch。void await()
:阻塞当前线程,直到计数器为零。void countDown()
:递减计数器的值,如果计数器值变为零,则释放所有等待的线程。
2. CyclicBarrier(同步屏障)了解吗?
CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一 组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
它和 CountDownLatch 类似,都可以协调多线程的结束动作,在它们结束后都可以执行特定动作,但是为什么要有 CyclicBarrier,自然是它有和 CountDownLatch 不同的地方。
不知道你听没听过一个新人 UP 主小约翰可汗,小约翰生平有两大恨——“想结衣结衣不依,迷爱理爱理不理。”我们来还原一下事情的经过:小约翰在亲政后认识了新垣结衣,于是决定第一次选妃,向结衣表白,等待回应。然而新垣结衣回应嫁给了星野源,小约翰伤心欲绝,发誓生平不娶,突然发现了铃木爱理,于是小约翰决定第二次选妃,求爱理搭理,等待回应。
我们拿代码模拟这一场景,发现 CountDownLatch 无能为力了,因为 CountDownLatch 的使用是一次性的,无法重复利用,而这里等待了两次。此时,我们用 CyclicBarrier 就可以实现,因为它可以重复利用。
运行结果:
CyclicBarrier 最最核心的方法,仍然是 await():
- 如果当前线程不是第一个到达屏障的话,它将会进入等待,直到其他线程都到达,除非发生被中断、屏障被拆除、屏障被重设等情况;
上面的例子抽象一下,本质上它的流程就是这样就是这样:
3. CyclicBarrier 和 CountDownLatch 有什么区别?
两者最核心的区别[18]:
- CountDownLatch 是一次性的,而 CyclicBarrier 则可以多次设置屏障,实现重复利用;
- CountDownLatch 中的各个子线程不可以等待其他线程,只能完成自己的任务;而 CyclicBarrier 中的各个线程可以等待其他线程
它们区别用一个表格整理:
CyclicBarrier | CountDownLatch |
---|---|
CyclicBarrier 是可重用的,其中的线程会等待所有的线程完成任务。届时,屏障将被拆除,并可以选择性地做一些特定的动作。 | CountDownLatch 是一次性的,不同的线程在同一个计数器上工作,直到计数器为 0. |
CyclicBarrier 面向的是线程数 | CountDownLatch 面向的是任务数 |
在使用 CyclicBarrier 时,你必须在构造中指定参与协作的线程数,这些线程必须调用 await()方法 | 使用 CountDownLatch 时,则必须要指定任务数,至于这些任务由哪些线程完成无关紧要 |
CyclicBarrier 可以在所有的线程释放后重新使用 | CountDownLatch 在计数器为 0 时不能再使用 |
在 CyclicBarrier 中,如果某个线程遇到了中断、超时等问题时,则处于 await 的线程都会出现问题 | 在 CountDownLatch 中,如果某个线程出现问题,其他线程不受影响 |
4. Semaphore(信号量)了解吗?
Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。
听起来似乎很抽象,现在汽车多了,开车出门在外的一个老大难问题就是停车 。停车场的车位是有限的,只能允许若干车辆停泊,如果停车场还有空位,那么显示牌显示的就是绿灯和剩余的车位,车辆就可以驶入;如果停车场没位了,那么显示牌显示的就是绿灯和数字 0,车辆就得等待。如果满了的停车场有车离开,那么显示牌就又变绿,显示空车位数量,等待的车辆就能进停车场。
我们把这个例子类比一下,车辆就是线程,进入停车场就是线程在执行,离开停车场就是线程执行完毕,看见红灯就表示线程被阻塞,不能执行,Semaphore 的本质就是协调多个线程对共享资源的获取。
我们再来看一个 Semaphore 的用途:它可以用于做流量控制,特别是公用资源有限的应用场景,比如数据库连接。
假如有一个需求,要读取几万个文件的数据,因为都是 IO 密集型任务,我们可以启动几十个线程并发地读取,但是如果读到内存后,还需要存储到数据库中,而数据库的连接数只有 10 个,这时我们必须控制只有 10 个线程同时获取数据库连接保存数据,否则会报错无法获取数据库连接。这个时候,就可以使用 Semaphore 来做流量控制,如下:
public class SemaphoreTest {private static final int THREAD_COUNT = 30;private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT);private static Semaphore s = new Semaphore(10);public static void main(String[] args) {for (int i = 0; i < THREAD_COUNT; i++) {threadPool.execute(new Runnable() {@Overridepublic void run() {try {s.acquire();System.out.println("save data");s.release();} catch (InterruptedException e) {}}});}threadPool.shutdown();}
}
在代码中,虽然有 30 个线程在执行,但是只允许 10 个并发执行。Semaphore 的构造方法 Semaphore(int permits
)接受一个整型的数字,表示可用的许可证数量。Semaphore(10)
表示允许 10 个线程获取许可证,也就是最大并发数是 10。Semaphore 的用法也很简单,首先线程使用 Semaphore 的 acquire()方法获取一个许可证,使用完之后调用 release()方法归还许可证。还可以用 tryAcquire()方法尝试获取许可证。
5. Exchanger 了解吗?
Exchanger(交换者)是一个用于线程间协作的工具类。Exchanger 用于进行线程间的数据交换。它提供一个同步点,在这个同步点,两个线程可以交换彼此的数据。
这两个线程通过 exchange 方法交换数据,如果第一个线程先执行 exchange()方法,它会一直等待第二个线程也执行 exchange 方法,当两个线程都到达同步点时,这两个线程就可以交换数据,将本线程生产出来的数据传递给对方。
Exchanger 可以用于遗传算法,遗传算法里需要选出两个人作为交配对象,这时候会交换两人的数据,并使用交叉规则得出 2 个交配结果。Exchanger 也可以用于校对工作,比如我们需要将纸制银行流水通过人工的方式录入成电子银行流水,为了避免错误,采用 AB 岗两人进行录入,录入到 Excel 之后,系统需要加载这两个 Excel,并对两个 Excel 数据进行校对,看看是否录入一致。
public class ExchangerTest {private static final Exchanger<String> exgr = new Exchanger<String>();private static ExecutorService threadPool = Executors.newFixedThreadPool(2);public static void main(String[] args) {threadPool.execute(new Runnable() {@Overridepublic void run() {try {String A = "银行流水A"; // A录入银行流水数据exgr.exchange(A);} catch (InterruptedException e) {}}});threadPool.execute(new Runnable() {@Overridepublic void run() {try {String B = "银行流水B"; // B录入银行流水数据String A = exgr.exchange("B");System.out.println("A和B数据是否一致:" + A.equals(B) + ",A录入的是:"+ A + ",B录入是:" + B);} catch (InterruptedException e) {}}});threadPool.shutdown();}
}
假如两个线程有一个没有执行 exchange()方法,则会一直等待,如果担心有特殊情况发生,避免一直等待,可以使用exchange(V x, long timeOut, TimeUnit unit)
设置最大等待时长。
6. 能说一下 ConcurrentHashMap 的实现吗?(补充)
ConcurrentHashMap
是 HashMap 的线程安全版本。
在 JDK 7 时采用的是分段锁机制(Segment Locking),整个 Map 被分为若干段,每个段都可以独立地加锁。因此,不同的线程可以同时操作不同的段,从而实现并发访问。
在 JDK 8 及以上版本中,ConcurrentHashMap 的实现进行了优化,不再使用分段锁,而是使用了一种更加精细化的锁——桶锁,以及 CAS 无锁算法。每个桶(Node 数组的每个元素)都可以独立地加锁,从而实现更高级别的并发访问。
对于读操作,通常不需要加锁,可以直接读取,ConcurrentHashMap 内部使用了 volatile 变量来保证内存可见性。
对于写操作,ConcurrentHashMap 使用 CAS 操作来实现无锁的更新,这是一种乐观锁的实现,因为它假设没有冲突发生,在实际更新数据时才检查是否有其他线程在尝试修改数据,如果有,采用悲观的锁策略,如 synchronized 代码块来保证数据的一致性。
说一下 JDK 7 中的 ConcurrentHashMap 的实现原理?
JDK 7 的 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组构成的。Segment 是一种可重入的锁 ReentrantLock
,HashEntry 则用于存储键值对数据。
一个 ConcurrentHashMap
里包含一个 Segment 数组,Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得它对应的 Segment 锁。
①、put 流程
ConcurrentHashMap 的 put 流程和 HashMap 非常类似,只不过是先定位到具体的 Segment,然后通过 ReentrantLock 去操作而已。
- 计算 hash,定位到 segment,segment 如果是空就先初始化;
- 使用 ReentrantLock 加锁,如果获取锁失败则尝试自旋,自旋超过次数就阻塞获取,保证一定能获取到锁;
- 遍历 HashEntry,key 相同就直接替换,不存在就插入。
- 释放锁。
②、get 流程
get 也很简单,通过 hash(key)
定位到 segment,再遍历链表定位到具体的元素上,需要注意的是 value 是 volatile
的,所以 get 是不需要加锁的。
说一下 JDK 8 中的 ConcurrentHashMap 的实现原理?
JDK 8 中的 ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS + synchronized 来保证并发安全性,整个容器只分为一个 Segment,即 table 数组。
Node 和 JDK 7 一样,使用 volatile 关键字,保证多线程操作时,变量的可见性。
ConcurrentHashMap 实现线程安全的关键点在于 put 流程。
①、put 流程
一句话:通过计算键的哈希值确定存储位置,如果桶为空,使用
CAS
插入节点;如果存在冲突,通过链表或红黑树插入。在冲突时,如果CAS
操作失败,会退化为synchronized
操作。写操作可能触发扩容或链表转为红黑树。
第一步,计算 hash,遍历 node 数组,如果 node 是空的话,就通过 CAS+自旋的方式初始化。
// 准备初始化
tab = initTable();
// 具体实现
private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;while ((tab = table) == null || tab.length == 0) {//如果正在初始化或者扩容if ((sc = sizeCtl) < 0)//等待Thread.yield(); // lost initialization race; just spinelse if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //CAS操作try {if ((tab = table) == null || tab.length == 0) {int n = (sc > 0) ? sc : DEFAULT_CAPACITY;@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = tab = nt;sc = n - (n >>> 2);}} finally {sizeCtl = sc;}break;}}return tab;
}
第二步,如果当前数组位置是空,直接通过 CAS 自旋写入数据。
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,Node<K,V> c, Node<K,V> v) {return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
第三步,如果 hash==MOVED
,说明需要扩容。
else if ((fh = f.hash) == MOVED)tab = helpTransfer(tab, f);
扩容的具体实现:
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {Node<K,V>[] nextTab; // 下一个表的引用,即新的扩容后的数组int sc; // 用于缓存sizeCtl的值// 检查条件:传入的表不为空,节点f是ForwardingNode类型,且f中的nextTable不为空if (tab != null && (f instanceof ForwardingNode) &&(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {int rs = resizeStamp(tab.length); // 根据当前表长度计算resize stamp// 检查循环条件:nextTab等于nextTable,table等于传入的tab,且sizeCtl为负数(表示正在进行或准备进行扩容)while (nextTab == nextTable && table == tab &&(sc = sizeCtl) < 0) {// 检查是否应该停止扩容(比如:resize stamp不匹配,或者已达到最大并发扩容线程数,或者transferIndex已经不大于0)if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||sc == rs + MAX_RESIZERS || transferIndex <= 0)break;// 尝试通过CAS增加sizeCtl的值,以表示有更多线程参与扩容if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {transfer(tab, nextTab); // 调用transfer方法,实际进行数据迁移break;}}return nextTab; // 返回新的表引用}return table; // 如果不符合扩容协助条件,返回当前表引用
}
第四步,如果都不满足,就使用 synchronized 写入数据,和 HashMap 一样,key 的 hash 一样就覆盖,反之使用拉链法解决哈希冲突,当链表长度超过 8 就转换成红黑树。
ConcurrentHashmap JDK 8 put 流程图:
②、get 查询
通过计算哈希值快速定位桶,在桶中查找目标节点,多个 key 值时链表遍历和红黑树查找。读操作是无锁的,依赖 volatile 保证线程可见性。
get 查询的时候,也是通过 key 的 hash 进行定位,需要注意的是 ConcurrentHashMap 会判断 hash 值是否小于 0。
如果小于 0,说明是个特殊节点,会调用节点的 find 方法进行查找,比如说 ForwardingNode 的 find 方法或者 TreeNode 的 find 方法。
总结一下 HashMap 和 ConcurrentHashMap 的区别?
①、HashMap 是非线程安全的,多线程环境下应该使用 ConcurrentHashMap。
②、由于 HashMap 仅在单线程环境下使用,所以不需要考虑同步问题,因此效率高于 ConcurrentHashMap。
你项目中怎么使用 ConcurrentHashMap 的?
在项目中,很多地方都用到了 ConcurrentHashMap
,比如说在异步工具类 AsyncUtil 中,使用 ConcurrentHashMap
来存储任务的名称和它们的运行时间,以便观察和分析任务的执行情况。
ConcurrentHashMap 对 HashMap 的优化?
ConcurrentHashMap
是 HashMap 的线程安全版本,使用了 CAS、synchronized、volatile 来确保线程安全。
首先是 hash 的计算方法上,ConcurrentHashMap 的 spread 方法接收一个已经计算好的 hashCode,然后将这个哈希码的高 16 位与自身进行异或运算,这里的 HASH_BITS 是一个常数,值为 0x7fffffff,它确保结果是一个非负整数。
static final int spread(int h) {return (h ^ (h >>> 16)) & HASH_BITS;
}
比 HashMap 的 hash 计算多了一个 & HASH_BITS
的操作。
static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
另外,ConcurrentHashMap 对节点 Node 做了进一步的封装,比如说用 Forwarding Node 来表示正在进行扩容的节点。
static final class ForwardingNode<K,V> extends Node<K,V> {final Node<K,V>[] nextTable;ForwardingNode(Node<K,V>[] tab) {super(MOVED, null, null, null);this.nextTable = tab;}
}
最后就是 put 方法,通过 CAS + synchronized 来保证线程安全。
为什么 ConcurrentHashMap 在 JDK 1.7 中要用 ReentrantLock,而在 JDK 1.8 要用 synchronized
ConcurrentHashMap 在 JDK 1.7 和 JDK 1.8 中的实现机制不同,主要体现在锁的机制上。
JDK 1.7 中的 ConcurrentHashMap 使用了分段锁机制,即 Segment 锁,每个 Segment 都是一个 ReentrantLock,这样可以保证每个 Segment 都可以独立地加锁,从而实现更高级别的并发访问。
而在 JDK 1.8 中,ConcurrentHashMap 取消了 Segment 分段锁,采用了更加精细化的锁——桶锁,以及 CAS 无锁算法,每个桶(Node 数组的每个元素)都可以独立地加锁,从而实现更高级别的并发访问。
再加上 JVM 对 synchronized 做了大量优化,如锁消除、锁粗化、自旋锁和偏向锁等,在低中等的竞争情况下,synchronized 的性能并不比 ReentrantLock 差,并且使用 synchronized 可以简化代码实现。
7. ConcurrentHashMap 怎么保证可见性?(补充)
2024 年 03 月 25 日增补
ConcurrentHashMap 保证可见性主要通过使用 volatile 关键字和 synchronized 同步块。
在 Java 中,volatile 关键字保证了变量的可见性,即一个线程修改了一个 volatile 变量后,其他线程可以立即看到这个修改。在 ConcurrentHashMap 的内部实现中,有些关键的变量被声明为 volatile,比如 Segment 数组和 Node 数组等。
此外,ConcurrentHashMap 还使用了 synchronized 同步块来保证复合操作的原子性。当一个线程进入 synchronized 同步块时,它会获得锁,然后执行同步块内的代码。当它退出 synchronized 同步块时,它会释放锁,并将在同步块内对共享变量的所有修改立即刷新到主内存,这样其他线程就可以看到这些修改了。
通过这两种机制,ConcurrentHashMap 保证了在并发环境下的可见性,从而确保了线程安全。
8. 为什么 ConcurrentHashMap 比 Hashtable 效率高(补充)
Hashtable 在任何时刻只允许一个线程访问整个 Map,通过对整个 Map 加锁来实现线程安全。
而 ConcurrentHashMap(尤其是在 JDK 8 及之后版本)通过锁分离和 CAS 操作实现更细粒度的锁定策略,允许更高的并发。
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,Node<K,V> c, Node<K,V> v) {return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
CAS 操作是一种乐观锁,它不会阻塞线程,而是在更新时检查是否有其他线程已经修改了数据,如果没有就更新,如果有就重试。
ConcurrentHashMap 允许多个读操作并发进行而不加锁,因为它通过 volatile 变量来保证读取操作的内存可见性。相比之下,Hashtable 对读操作也加锁,增加了开销。
public V get(Object key) {Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;// 1. 重hashint h = spread(key.hashCode());if ((tab = table) != null && (n = tab.length) > 0 &&(e = tabAt(tab, (n - 1) & h)) != null) {// 2. table[i]桶节点的key与查找的key相同,则直接返回if ((eh = e.hash) == h) {if ((ek = e.key) == key || (ek != null && key.equals(ek)))return e.val;}// 3. 当前节点hash小于0说明为树节点,在红黑树中查找即可else if (eh < 0)return (p = e.find(h, key)) != null ? p.val : null;while ((e = e.next) != null) {//4. 从链表中查找,查找到则返回该节点的value,否则就返回null即可if (e.hash == h &&((ek = e.key) == key || (ek != null && key.equals(ek))))return e.val;}}return null;
}
9. 能说一下 CopyOnWriteArrayList 的实现原理吗?(补充)
CopyOnWriteArrayList
是一个线程安全的 ArrayList,它遵循写时复制(Copy-On-Write)的原则,即在写操作时,会先复制一个新的数组,然后在新的数组上进行写操作,写完之后再将原数组引用指向新数组。
这样,读操作总是在一个不变的数组版本上进行的,就不需要同步了。
10. 能说一下 BlockingQueue 吗?(补充)
2024 年 08 月 18 日增补,从集合框架移动到并发编程这里
BlockingQueue
代表的是线程安全的队列,不仅可以由多个线程并发访问,还添加了等待/通知机制,以便在队列为空时阻塞获取元素的线程,直到队列变得可用,或者在队列满时阻塞插入元素的线程,直到队列变得可用。
阻塞队列(BlockingQueue
)被广泛用于“生产者-消费者”问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。
BlockingQueue
接口的实现类有 ArrayBlockingQueue
、DelayQueue
、LinkedBlockingDequ
e、LinkedBlockingQueue
、LinkedTransferQueue
、PriorityBlockingQueue
、SynchronousQueue
等。
阻塞指的是一种程序执行状态,其中某个线程在等待某个条件满足时暂停其执行(即阻塞),直到条件满足时恢复其执行。
阻塞队列是如何实现的?
就拿 ArrayBlockingQueue
来说,它是一个基于数组的有界阻塞队列,采用 ReentrantLock
锁来实现线程的互斥,而 ReentrantLock
底层采用的是 AQS
实现的队列同步,线程的阻塞调用 LockSupport.park
实现,唤醒调用 LockSupport.unpark
实现。
public void put(E e) throws InterruptedException {checkNotNull(e);// 使用ReentrantLock锁final ReentrantLock lock = this.lock;// 获取锁lock.lockInterruptibly();try {// 如果队列已满,阻塞while (count == items.length)notFull.await();// 插入元素enqueue(e);} finally {// 释放锁lock.unlock();}
}/*** 插入元素*/
private void enqueue(E x) {final Object[] items = this.items;items[putIndex] = x;if (++putIndex == items.length)putIndex = 0;count++;// 插入元素后,通知消费者线程可以继续取元素notEmpty.signal();
}/*** 获取元素*/
public E take() throws InterruptedException {final ReentrantLock lock = this.lock;// 获取锁lock.lockInterruptibly();try {// 如果队列为空,阻塞,等待生产者线程放入元素while (count == 0)notEmpty.await();// 移除元素并返回return dequeue();} finally {lock.unlock();}
}/*** 移除元素并返回*/
private E dequeue() {final Object[] items = this.items;@SuppressWarnings("unchecked")E x = (E) items[takeIndex];items[takeIndex] = null;// 数组是循环队列,如果到达数组末尾,从头开始if (++takeIndex == items.length)takeIndex = 0;count--;if (itrs != null)itrs.elementDequeued();// 移除元素后,通知生产者线程可以继续放入元素notFull.signal();return x;
}
相关文章:
Java并发编程面试题:并发工具类(10题)
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编…...
【Oracle专栏】扩容导致数据文件 dbf 丢失,实操
Oracle相关文档,希望互相学习,共同进步 风123456789~-CSDN博客 1.背景 同事检查扩容情况,发现客户扩容后数据盘后,盘中原有文件丢失,再检查发现数据库没有启动。通过检查发现数据盘中丢失的是oracle的 dbf 表空间文件。数据库无法启动。 检查情况:1)没有rman备份 …...
【Linux】Linux 的管道与重定向的理解
目录 一、了解Linux目录配置标准FHS 二、Linux数据重定向的理解与操作 2.1基本背景 2.2重定向的理解 2.3Linux管道命令的理解与操作 三、Linux 环境变量与PATH 3.1环境变量PATH 一、了解Linux目录配置标准FHS FHS本质:是一套规定Linux目录结构,软…...
【交互 / 差分约束】
题目 代码 #include <bits/stdc.h> using namespace std; using ll long long;const int N 10510; const int M 200 * 500 10; int h[N], ne[M], e[M], w[M], idx; ll d[N]; int n, m; bool st[N]; int cnt[N];void add(int a, int b, int c) {w[idx] c, e[idx] b…...
1. Go 语言环境安装
👑 博主简介:高级开发工程师 👣 出没地点:北京 💊 人生目标:自由 ——————————————————————————————————————————— 版权声明:本文为原创文章…...
灰度图像和RGB图像在数据大小和编码处理方式差别
技术背景 好多开发者对灰度图像和RGB图像有些认知差异,今天我们大概介绍下二者差别。灰度图像(Grayscale Image)和RGB图像在编码处理时,数据大小和处理方式的差别主要体现在以下几个方面: 1. 通道数差异 图像类型通道…...
Lord Of The Root: 1.0.1通关
Lord Of The Root: 1.0.1 来自 <Lord Of The Root: 1.0.1 ~ VulnHub> 1,将两台虚拟机网络连接都改为NAT模式 2,攻击机上做namp局域网扫描发现靶机 nmap -sn 192.168.23.0/24 那么攻击机IP为192.168.23.182,靶场IP192.168.23.247 3&…...
虚拟机安装CentOS7网络问题
虚拟机安装CentOS7网络问题 1. 存在的问题1.1 CentOS7详细信息 2. 解决问题3.Windows下配置桥接模式 1. 存在的问题 虽然已经成功在虚拟机上安装了CentOS7,但是依旧不能上网。 1.1 CentOS7详细信息 [fanzhencentos01 ~]$ hostnamectlStatic hostname: centos01Ic…...
AI-02a5a6.神经网络-与学习相关的技巧-批量归一化
批量归一化 Batch Normalization 设置合适的权重初始值,则各层的激活值分布会有适当的广度,从而可以顺利的进行学习。那么,更进一步,强制性的调整激活值的分布,是的各层拥有适当的广度呢?批量归一化&#…...
matlab实现蚁群算法解决公交车路径规划问题
使用蚁群算法解决公交车路径规划问题的MATLAB代码实现,包含详细注释和仿真流程。该算法以站点间行驶时间或距离为优化目标,寻找最优公交路线。 1. 问题建模与参数设置 1.1 输入数据 站点坐标:假设有20个公交站点,随机生成位置。…...
Agent Builder API - Agent Smith 扩展的后端服务(开源代码)
一、软件介绍 文末提供程序和源码下载 Agent Builder API - Agent Smith 扩展的后端服务(开源代码)手动设置:在本地计算机中克隆此存储库并启动 python FAST API 服务器。(可选)安装并设置 Mongo DB。Dev Container…...
C++类和对象之相关特性
C 一.类型转换隐式类型转换 二.static成员一、static成员变量二、static成员函数三、static成员的存储位置四、总结 三.友元一、友元函数二、友元类三、友元成员函数四、总结 四.内部类五.匿名对象六.new 一.类型转换 在C中,类类型转换是指将一个类的对象转换为另一…...
容器编排的革命:Kubernetes如何引领IT的云原生时代
文章目录 Kubernetes的本质:容器世界的智能指挥家Kubernetes的核心功能包括: Kubernetes的演进:从谷歌的实验到全球标准核心技术:Kubernetes的基石与生态1. Pod与容器:最小调度单位2. Deployment:无状态应用…...
2025视频协作工具全景解析:技术跃迁与场景重构
一、技术演进:从功能工具到智能生态 2025年视频协作软件的核心竞争力已从基础功能升级为技术生态的构建。以分秒帧为例,其音视频生产协作系统,可帮助创作者在云端构建工作流,让跨地域、跨终端、跨团队的协作组可以在统一的安全平台上管理所有媒体资源、任务、反馈信息,从而更高…...
保持视频二维码不变,更新视频的内容
视频替换功能允许用户在保持视频二维码、观看地址和调用代码不变的情况下更新视频内容,从而节省重新印刷物料的成本。这一功能适用于多种场景,如营销宣传、产品操作手册、设备说明书等,当视频内容需要修改或更新时,用户只需上传新…...
Linux常用命令40——alias设置命令别名
在使用Linux或macOS日常开发中,熟悉一些基本的命令有助于提高工作效率,alias命令来自英文单词alias,中文译为“别名”,其功能是设置命令别名信息。我们可以使用alias将一些较长的命令进行简写,往往几十个字符的命令会变…...
Java大师成长计划之第22天:Spring Cloud微服务架构
📢 友情提示: 本文由银河易创AI(https://ai.eaigx.com)平台gpt-4o-mini模型辅助创作完成,旨在提供灵感参考与技术分享,文中关键数据、代码与结论建议通过官方渠道验证。 随着企业应用的不断扩展,…...
为什么go语言中返回的指针类型,不需要用*取值(解引用),就可以直接赋值呢?
Go 中返回的是指针,但你却能直接用“.”访问字段,看起来像是“没有解引用”,其实是 Go 帮你自动处理了“指针解引用”的语法糖。 在 Go 中,如果你有一个结构体指针(例如 *FileMeta),你可以直接…...
Java生成可控的Word表格功能开发
在日常办公自动化与系统集成场景中,生成结构化的Word文档已成为一种刚性需求,尤其是带有格式规范、内容动态填充的Word表格(Table)。本文将围绕如何利用Java开发一个可控的Word表格生成功能模块展开,涵盖技术选型、代码实现、边界控制与常见问题处理等方面,帮助开发者快速…...
OpenCV CUDA 模块中用于在 GPU 上计算矩阵中每个元素的绝对值或复数的模函数abs()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 void cv::cuda::abs(InputArray src, OutputArray dst, Stream &stream Stream::Null()) 是 OpenCV 的 CUDA 模块中的一个函数,…...
hadoop知识点
(一)复制和移动 1.复制文件 格式:cp源文件 目标文件 示例:把filel.txt复制一份得到file2.txt 2.复制目录 格式:cp-r源文件夹 目标文件夹 示例:把目标dir1复制一份得到dir2 3.重命名和移动 格式:…...
最短路与拓扑(2)
1、信使 #include<bits/stdc.h> using namespace std; const int N105; int n,m; int g[N][N]; int dist[N]; bool st[N]; const int INF0x3f3f3f3f;int dij(){memset(dist,0x3f,sizeof dist);dist[1]0;for(int i1;i<n;i){int t0;for(int j1;j<n;j){if(!st[j]&…...
LLaMA-Factory 微调 Qwen2-7B-Instruct
一、系统环境 使用的 autoDL 算力平台 1、下载基座模型 pip install -U huggingface_hub export HF_ENDPOINThttps://hf-mirror.com # (可选)配置 hf 国内镜像站huggingface-cli download --resume-download shenzhi-wang/Llama3-8B-Chinese-Chat -…...
濒危仙草的重生叙事:九仙尊米斛花节如何以雅集重构中医药文化IP
五月的霍山深处,层峦叠翠之间,中华仙草霍山米斛迎来一年一度的花期。九仙尊以“斛韵雅集,春野茶会”为主题,举办为期半月的米斛花文化节,融合中医药文化、东方美学与自然体验,打造一场跨越古今的沉浸式文化盛宴。活动涵盖古琴雅集、书法创作、茶道冥想、诗歌吟诵、民族歌舞等多…...
Pomelo知识框架
一、Pomelo 基础概念 Pomelo 简介 定位:分布式游戏服务器框架(网易开源)。 特点:高并发、可扩展、多进程架构、支持多种通信协议(WebSocket、TCP等)。 适用场景:MMO RPG、实时对战、社交游戏等…...
歌词滚动效果
<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><!-- 设置标签页图标 --><link rel"shortcut icon&…...
python如何合并excel单元格
在Python中合并Excel单元格,常用openpyxl库实现。以下是详细步骤和示例代码: 方法一:使用 openpyxl 库 步骤说明: 安装库: pip install openpyxl导入库并加载文件: from openpyxl import load_workbook# …...
嵌入式学习笔记 D20 :单向链表的基本操作
单向链表的创建单向链表的插入单向链表的删除及清空单向链表的修改单向链表的查找单向链表的逆序 一、单向链表的创建 LinkList *CreateLinkList() {LinkList *ll malloc(sizeof(LinkList));if (NULL ll) {fprintf(stderr, "CreateLink malloc");return NULL;}ll…...
瀑布模型VS敏捷模型VS喷泉模型
目录 1. 瀑布模型(Waterfall Model) 2. 敏捷模型(Agile Model) 3. 喷泉模型(Fountain Model)...
Android usb网络共享详解
Android usb网络共享详解 文章目录 Android usb网络共享详解一、前言二、USB网络共享使用的前提1、Android设备支持adb 并且打开usb开关2、原生Settings能看到USB网络共享开关3、代码中检测USB网络共享是否支持 三、Settings 中USB网络共享代码的部分代码1、Settings\res\xml\t…...
在线黑白图像转换:简单却强大的视觉表达工具
为什么选择黑白图像? 在这个色彩缤纷的数字世界中,黑白摄影却始终保持着其独特的魅力。黑白图像消除了色彩的干扰,让我们更专注于构图、纹理和形式的表达。这种经典的转换技术能够创造出富有情感和强烈对比的视觉作品,呈现出彩色…...
python 异步执行测试
1. 并行执行机制 子进程级并行:通过 asyncio.create_subprocess_exec 启动的每个外部命令(如 python run_spider.py)会创建一个独立的系统进程,由操作系统直接调度,实现真正的并行执行。 协程级并发:主程序…...
《Python星球日记》 第69天:生成式模型(GPT 系列)
名人说:路漫漫其修远兮,吾将上下而求索。—— 屈原《离骚》 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 目录 一、GPT简介:从架构到原理1. GPT的架构与工作原理2. Decoder-only结…...
STM32 之网口资源
1 网口资源介绍 STM32F407 是 STMicroelectronics 推出的高性能 ARM Cortex-M4 微控制器,具备多种外设接口,其中包括一个 Ethernet MAC 控制器(带 IEEE 1588 支持)。这意味着你可以使用 STM32F407 实现网络通信功能(通…...
一分钟在Cherry Studio和VSCode集成火山引擎veimagex-mcp
MCP的出现打通了AI模型和外部数据库、网页API等资源,成倍提升工作效率。近期火山引擎团队推出了 MCP Server SDK: veimagex-mcp。本文介绍如何在Cherry Studio 和VSCode平台集成 veimagex-mcp。 什么是MCP MCP(Model Context Protocol&…...
业务中台-典型技术栈选型(微服务、容器编排、分布式数据库、消息队列、服务监控、低代码等)
在企业数字化中台建设中,业务中台是核心支撑平台,旨在通过技术手段将企业核心业务能力抽象、标准化和复用,以快速响应前端业务需求。其核心技术流涉及从业务抽象到服务化、治理和持续优化的全流程。以下是业务中台建设中的核心技术体系及关键…...
图像颜色理论与数据挖掘应用的全景解析
文章目录 一、图像颜色系统的理论基础1.1 图像数字化的本质逻辑1.2 颜色空间的数学框架1.3 量化过程的技术原理 二、主要颜色空间的深度解析2.1 RGB颜色空间的加法原理2.2 HSV颜色空间的感知模型2.3 CMYK颜色空间的减色原理 三、图像几何属性与高级特征3.1 分辨率与像素密度的关…...
从规则驱动到深度学习:自然语言生成的进化之路
自然语言生成技术正经历着人类文明史上最剧烈的认知革命。这项起源于图灵测试的技术,已经从简单的符号操作演变为具备语义理解能力的智能系统。当我们回溯其发展历程,看到的不仅是算法模型的迭代更新,更是一部人类认知自我突破的史诗。这场革…...
影刀RPA网页自动化总结
1. 影刀RPA网页自动化概述 1.1 定义与核心功能 影刀RPA网页自动化是一种通过软件机器人模拟人类操作网页行为的技术,旨在提高网页操作效率、减少人工干预。其核心功能包括: 网页数据抓取:能够高效抓取网页上的数据,如电商数据、…...
[:, :, 1]和[:, :, 0] 的区别; `prompt_vector` 和 `embedding_matrix`的作用
prompt_vector = torch.sum(prompt_embedding * attention_weights.unsqueeze(-1), dim=1) # [1, hidden_dim] prompt_vector = torch.sum(prompt_embedding * attention_weights.unsqueeze(-1), dim=1) 主要作用是通过将 prompt_embedding 与 attention_weights 相乘后再按指…...
LeetCode 题解 41. 缺失的第一个正数
41. 缺失的第一个正数 给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。 示例 1: 输入:nums [1,2,0] 输出:3 解释:范围 [1,…...
3337. 字符串转换后的长度 II
3337. 字符串转换后的长度 II # 定义了一个大质数 MOD,用于取模运算,防止数值溢出。 MOD 1_000_000_007# 矩阵乘法 mul def mul(a:List[List[int]], b:List[List[int]]) -> List[List[int]]:# 输入两个矩阵 a 和 b,返回它们的矩阵乘积 a…...
基于 TensorFlow 框架的联邦学习可穿戴设备健康数据个性化健康管理平台研究
基于 TensorFlow 框架的联邦学习可穿戴设备健康数据个性化健康管理平台研究 摘要: 随着可穿戴设备的普及,人们对于自身健康管理的需求日益增长。然而,可穿戴设备所收集的健康数据往往分散在不同用户的设备中,且涉及用户隐私敏感信息。本研究旨在构建一个基于 TensorFlow 框…...
查看字节真实二进制形式示例解析1
查看字节的真实二进制形式? 若需要显式查看二进制0/1,可以通过以下方法转换: 方法1:逐字节转换为二进制字符串 def bytes_to_binary(data: bytes) -> str:return .join([bin(byte)[2:].zfill(8) for byte in data])# 示例 …...
hadoop中spark基本介绍
Spark是一个基于内存计算的快速、通用、可扩展的大数据处理引擎,可与Hadoop集成并在其生态系统中发挥重要作用。以下是其基本介绍: 特点 - 快速:基于内存计算,能将中间结果缓存在内存中,避免频繁读写磁盘,大…...
Apollo学习——键盘控制速度
# keyboard_control.py import time import keyboard # 键盘输入模块 pip install keyboard from getkey import getkey, keys from cyber.python.cyber_py3 import cyber_time from cyber.python.cyber_py3 import cyber from modules.common_msgs.control_msgs import contro…...
无人机数据处理与特征提取技术分析!
一、运行逻辑 1. 数据采集与预处理 多传感器融合:集成摄像头、LiDAR、IMU、GPS等传感器,通过硬件时间戳或PPS信号实现数据同步,确保时空一致性。 边缘预处理:在无人机端进行数据压缩(如JPEG、H.265)…...
Java内存马的检测与发现
【网络安全】Java内存马的检测与发现 一、Java内存马的现象二、检测思路三、重点关注类四、检测方法1. 检查方法(FindShell)2. 检查方法(sa-jdi)3. 检查方法(arthas-boot)4. 检查方法(cop.jar&a…...
基于策略的强化学习方法之策略梯度(Policy Gradient)详解
在前文中,我们已经深入探讨了Q-Learning、SARSA、DQN这三种基于值函数的强化学习方法。这些方法通过学习状态值函数或动作值函数来做出决策,从而实现智能体与环境的交互。 策略梯度是一种强化学习算法,它直接对策略进行建模和优化,…...
未来软件开发趋势与挑战
未来软件开发的方向将受到技术进步、市场需求和社会变革的多重影响。以下是可能主导行业发展的关键趋势: 1. AI与自动化深度整合 AI代码生成:GitHub Copilot等工具将进化成"AI开发伙伴",能理解业务逻辑并自动生成完整模块。自修复…...