💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
- 推荐:kuan 的首页,持续学习,不断总结,共同进步,活到老学到老
- 导航
- 檀越剑指大厂系列:全面总结 java 核心技术点,如集合,jvm,并发编程 redis,kafka,Spring,微服务,Netty 等
- 常用开发工具系列:罗列常用的开发工具,如 IDEA,Mac,Alfred,electerm,Git,typora,apifox 等
- 数据库系列:详细总结了常用数据库 mysql 技术点,以及工作中遇到的 mysql 问题等
- 懒人运维系列:总结好用的命令,解放双手不香吗?能用一个命令完成绝不用两个操作
- 数据结构与算法系列:总结数据结构和算法,不同类型针对性训练,提升编程思维,剑指大厂
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。💝💝💝 ✨✨ 欢迎订阅本专栏 ✨✨
博客目录
- 1.JUC 包概述?
- 2.并发编程逻辑
- 3.为什么用 ConcurrentHashMap?
- 4.ConcurrentHashMap 数据结构?
- 5.ConcurrentHashMap 初始化?
- 6.sizeCtl 的作用
- 7.定位数据?
- 8.ConcurrentHashMap 的 get 操作?
- 9.ConcurrentHashMap 的 put 操作?
- 10.说说 ConcurrentLinkedQueue?
- 11.BlockingQueue 的实现原理?
- 12.JDK 中阻塞队列
- 13.DelayQueue 的使用场景?
- 14.TransferQueue 的使用?
- 15.Fork/Join 框架原理?
- 16.fork 方法解读
- 17.join 方法解读
- 18.说说 java 中的原子操作类?
- 19.说说对 CountDownLatch 的理解?
- 20.同步屏障 CyclicBarrier 理解?
- 21. CyclicBarrier 和 CountDownLatch?
- 22.说说 Semaphore ?
- 23.CopyOnWriteArrayList 原理?
- 24.LongAdder 原理
1.JUC 包概述?
juc 是 java.util.concurrent 的简称,为了支持高并发任务,在编程时可以有效减少竞争条件和死锁线程.
juc 主要包含 5 大工具包
工具包 | 描述 |
locks | - ReentrantLock: 独占锁,同一时间只能被一个线程获取,支持重入性。 - ReentrantReadWriteLock: 读写锁,ReadLock 是共享锁,WriteLock 是独占锁。 - LockSupport: 提供阻塞和解除阻塞线程的功能,不会导致死锁。 |
executor | - Executor: 线程池的顶级接口。 - ExecutorService: 扩展了 Executor 接口,用于管理线程池的生命周期和任务执行。 - ScheduledExecutorService: 继承自 ExecutorService,支持周期性执行任务。 - ScheduledThreadPoolExecutor: ScheduledExecutorService 的实现类,用于周期性调度任务。 |
tools | - CountDownLatch: 一个同步辅助类,等待其他线程执行完任务后再执行。 - CyclicBarrier: 另一个同步辅助类,当所有线程都到达某个公共的屏障点时,才继续执行。 - Semaphore: 计数信号量,本质上是共享锁,通过 acquire() 获取信号量许可,通过 release() 释放许可。 |
atomic | - AtomicBoolean: 原子操作类,提供原子更新 boolean 值的功能。 - AtomicInteger: 原子操作类,提供原子更新 int 值的功能。 |
collections | - HashMap: 线程不安全的哈希表实现,对应的高并发类是 ConcurrentHashMap。 - ArrayList: 线程不安全的动态数组实现,对应的高并发类是 CopyOnWriteArrayList。 - HashSet: 线程不安全的哈希集合实现,对应的高并发类是 CopyOnWriteArraySet。 - Queue: 线程安全的队列实现,对应的高并发类是 ConcurrentLinkedQueue。 - SimpleDateFormat: 线程不安全的日期格式化类,对应的高并发类是 FastDateFormat 或者 DateFormatUtils。 |
请注意,JUC(Java.util.concurrent)是 Java 中处理并发编程的工具包,包含了大量用于处理多线程编程的类和接口。以上列出的工具包和类仅是其中的一部分,还有很多其他有用的工具供开发者使用。
2.并发编程逻辑
底层依赖—>中层工具—>具体实现
- 首先,声明共享变量为 volatile。
- 然后,使用 CAS 的原子条件更新来实现线程之间的同步。
- 同时,配合以 volatile 的读/写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信。
3.为什么用 ConcurrentHashMap?
ConcurrentHashmap 存在的原因:
- HashMap 线程不安全的 map,多线程环境下 put 操作会出现死循环.会导致 Entry 链表变为环形结构.next 节点用不为空,就成了死循环获取 Entry.
- HashTable 效率低下
- ConcurrentHashMap 锁分段(jdk1.7)可以提高并发访问效率
ConcurrentHashmap 和 HashMap 区别:
- HashMap 是非线程安全的,而 HashTable 和 ConcurrentHashMap 都是线程安全的
- HashMap 的 key 和 value 均可以为 null;而 HashTable 和 ConcurrentHashMap 的 key 和 value 均不可以为 null
- HashTable 和 ConcurrentHashMap 的区别:保证线程安全的方式不同,HashTable 是使用 synchronized,ConcurrentHashMap 在 jdk1.8 中使用 cas 和 synchronized
4.ConcurrentHashMap 数据结构?
jdk1.8 之前的数据结构:
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结合组成。默认有 16 个区段。Segment 是一种可重入锁(ReentrantLock),在 ConcurrentHashMap 里扮演锁的角色;HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组。Seqment 的结构和 HashMap 类似,是一种数组和链表结构。一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得与它对应的 Segment 锁.
不同线程操作不同的 Segment 就不会出现安全问题,性能上大提升.在 jdk1.8 之前,多线程操作同一个 Segment 不能并发,在 jdk1.8 优化了.
jdk1.8 之后的数据结构:
改进:没有 Segment 区段了,和 HashMap 一致了,数组+链表+红黑树 +乐观锁 + synchronized
ConcurrentHashMap 数据结构与 1.8 中的 HashMap 保持一致,均为数组+链表+红黑树,是通过乐观锁+Synchronized 来保证线程安全的.当多线程并发向同一个散列桶添加元素时。
若散列桶为空:
此时触发乐观锁机制,线程会获取到桶中的版本号,在添加节点之前,判断线程中获取的版本号与桶中实际存在的版本号是否一致,若一致,则添加成功,若不一致,则让线程自旋。
若散列桶不为空:
此时使用 synchronized 来保证线程安全,先访问到的线程会给桶中的头节点加锁,从而保证线程安全。
5.ConcurrentHashMap 初始化?
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
这段代码是用来创建一个 ConcurrentHashMap
(并发哈希表)实例的构造函数。让我们逐步解释它:
- 参数检查:
- 首先,代码对传入的参数进行检查,确保它们符合创建
ConcurrentHashMap
的要求。 -
loadFactor
:必须大于 0.0,因为它表示内部哈希表进行扩容的加载因子阈值。 -
initialCapacity
:必须大于等于 0,代表哈希表的初始容量。 -
concurrencyLevel
:必须大于 0,表示预估的并发更新线程数。
- 设置初始容量:
- 接下来,构造函数检查
initialCapacity
是否小于concurrencyLevel
。如果是,它会将initialCapacity
设置为concurrencyLevel
。 - 这个步骤确保哈希表至少有和预估的并发更新线程数一样多的桶(存储空间),以提高并发性能。
- 计算大小:
- 构造函数接着根据给定的
initialCapacity
和loadFactor
计算内部哈希表的大小。 -
size
的计算为1.0 + initialCapacity / loadFactor
。 - 这个大小用来确定哈希表的容量。
- 确定哈希表的容量:
- 接下来,构造函数使用计算得到的
size
来决定哈希表的容量。 - 如果计算得到的
size
大于等于MAXIMUM_CAPACITY
,则将哈希表的容量设置为MAXIMUM_CAPACITY
。 - 否则,会调用
tableSizeFor()
方法,根据size
计算出最接近且大于size
的 2 的幂次方值,并将其作为哈希表的容量。
- 设置
sizeCtl
:
- 最后,将计算得到的容量
cap
赋值给sizeCtl
,这是ConcurrentHashMap
内部用于管理表的控制参数。
总体来说,这段代码创建了一个 ConcurrentHashMap
的实例,并对传入的参数进行了合法性检查,然后根据参数计算了哈希表的大小和容量。这些控制参数的设置有助于确保并发性能和哈希表的正确功能。
6.sizeCtl 的作用
sizeCtl
是 ConcurrentHashMap
内部的一个控制参数,用于控制哈希表的状态和并发扩容操作。
具体来说,sizeCtl
的作用如下:
- 控制表的初始化大小:
- 在构造函数中,我们可以看到
sizeCtl
被用来设置哈希表的初始容量,它是在构造函数末尾通过this.sizeCtl = cap;
进行赋值的。 -
cap
是根据传入的initialCapacity
和loadFactor
计算得到的哈希表容量。
- 控制表的扩容:
-
sizeCtl
还用于控制哈希表的并发扩容操作。当哈希表需要进行扩容时,会使用sizeCtl
的值来判断当前是否有其他线程正在进行扩容操作,或者标记当前线程正在进行扩容。 - sizeCtl 的值可以是负数或者正数,具体的含义如下:
- 负数:表示当前有线程正在进行扩容操作,而绝对值表示正在扩容的线程数减 1(因为扩容线程计数是从-1 开始的)。
- 0:表示当前没有进行扩容操作,且可以接受新的扩容请求。
- 正数:表示当前没有进行扩容操作,但是已经有线程发起了扩容请求,该正数值表示发起扩容请求的线程数。
- 控制并发性:
-
sizeCtl
的负值和正值都表示当前存在扩容请求或正在扩容的情况,因此在进行扩容操作时,其他线程的访问可能需要等待。 - 这样的设计可以防止多个线程同时触发扩容操作,从而避免对哈希表的结构进行重复扩容,提高了并发性能。
总结起来,sizeCtl
是 ConcurrentHashMap
内部用于控制哈希表状态、扩容操作和并发性的重要参数。它在哈希表的初始化和扩容过程中起着关键的作用,保障了 ConcurrentHashMap
的正确性和高并发性能。
7.定位数据?
首先需要找到 segment,通过散列算法对 hashCode 进行再散列找到 segment,主要是为了分布更加均匀,否则都在一个 segment 上,起不到分段锁的作用.散列函数如下,为了让高位参与到运算中.
final Segment<K,V> segmentFor(int hash) {
return segments[(hash >>> segmentShift)&segmentMask];
}
hash >>> segmentShift)&segmentMask // 定位Segment所使用的hash 算法
int index= hash &(tab.length -1); // 定位HashEntry所使用的hash算法
8.ConcurrentHashMap 的 get 操作?
transient volatile int count;
volatile V value;
public V get(Object key) {
int hash =hash(key.hashCode());//先进行一次再散列
return segmentFor(hash).get(key,hash);//再通过散列值定位到segment
//最后通过散列获取到元素
}
get 操作的高效之处在于整个 get 过程不需要加锁。原因是它的 get 方法里将要使用的共享变量都定义成 volatile 类型
9.ConcurrentHashMap 的 put 操作?
put 方法必须加锁,需要做 2 件事
- 是否扩容
- 查找插入位置
在扩容的时候,首先会创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。为了高效, ConcurrentHashMap 不会对整个容器进行扩容,而只对某个 segment 进行扩容(jdk1.7)。数据量相对较小,注意和 HashMap 的区别.
10.说说 ConcurrentLinkedQueue?
线程安全的非阻塞队列 ConcurrentLinkedQueue
offer(E e)
方法是 ConcurrentLinkedQueue
提供的一个添加元素的方法。它用于将指定的元素插入队列的末尾(尾部)。
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {//p是最后一个节点
if (p.casNext(null, newNode)) {//通过cas替换节点p的next节点
if (p != t) // 是否通过cas替换tail的指向
casTail(t, newNode); // Failure is OK.
return true;
}
}
else if (p == q)//tail未初始化
p = (t != (t = tail)) ? t : head;
else
p = (p != t && t != (t = tail)) ? t : q;
}
}
详细解释 ConcurrentLinkedQueue
的 offer(E e)
方法:
- 参数:
E e
-
e
是要插入队列的元素。
- 功能:
-
offer(E e)
方法将元素e
插入队列的末尾(尾部)。 - 如果队列当前没有被其他线程占用或被修改,则插入操作是立即执行的。
- 如果队列当前正在被其他线程进行修改(比如扩容等操作),那么插入操作可能会被阻塞,直到队列可用为止。
- 返回值:
-
true
:如果元素e
成功插入队列。 -
false
:如果由于某种原因(例如队列已满)未能插入元素e
。
- 注意事项:
-
ConcurrentLinkedQueue
是一个无界队列,不会限制队列的容量,因此offer(E e)
永远不会阻塞,除非由于资源限制或其他异常情况导致的插入失败。 - 由于
ConcurrentLinkedQueue
是无界的,因此在不断地添加元素的情况下,它可能会占用大量的内存。
总的来说,ConcurrentLinkedQueue
的 offer(E e)
方法用于向队列的末尾添加元素 e
,它是一个非阻塞方法,可以在高并发环境下安全使用。需要注意的是,由于它是无界队列,添加元素时应当注意不要无限制地添加,以免占用过多的内存。
poll()
方法是ConcurrentLinkedQueue
的一个核心方法,用于从队列头部获取并移除元素。下面详细解释poll()
方法的工作原理:
-
poll()
方法用于从队列头部获取并移除元素。如果队列为空,poll()
方法会返回null
。 -
poll()
方法是一个非阻塞方法,它不会阻塞线程,即使队列为空。 - 在执行
poll()
方法时,它会检查队列头部是否有元素。 - 若队列为空(即没有任何元素),
poll()
方法会立即返回null
。 - 若队列非空,
poll()
方法会移除队列头部的元素,并将其返回。
public E poll() {
restartFromHead:
for (;;) {
for (Node<E> h = head, p = h, q;;) {
E item = p.item;
if (item != null && p.casItem(item, null)) {
if (p != h)
updateHead(h, ((q = p.next) != null) ? q : p);
return item;
}
else if ((q = p.next) == null) {
updateHead(h, p);
return null;
}
else if (p == q)
continue restartFromHead;
else
p = q;
}
}
}
11.BlockingQueue 的实现原理?
方法 | 1 | 2 | 3 | 4 |
插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除方法 | remove() | poll() | take() | poll(time,unit) |
检查方法 | element() | peek() |
阻塞队列使用 Lock+多个 Condition 实现的 FIFO 的队列.多线程环境下安全的,如果队列满了,放入元素的线程会被阻塞,如果队列空了,取元素的线程会被阻塞.具体原理一起看源代码。通过 Condition 来实现队列元素的阻塞,是空还是满.
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
12.JDK 中阻塞队列
JDK 7 提供了 7 个阻塞队列,如下。
队列类型 | 描述 |
ArrayBlockingQueue | 一个由数组结构组成的有界阻塞队列。它按照 FIFO(先进先出)原则对元素进行排序。在队列已满时,插入操作将被阻塞,直到队列有空间可用。在队列为空时,获取操作也将被阻塞,直到队列中有元素可获取。 |
LinkedBlockingQueue | 一个由链表结构组成的有界阻塞队列。也按照 FIFO 原则对元素进行排序。与 ArrayBlockingQueue 不同的是,它没有固定的容量上限,可以在构造时指定容量,如果未指定则默认为 Integer.MAX_VALUE。当队列为空或已满时,插入和获取操作将被阻塞。 |
PriorityBlockingQueue | 一个支持优先级排序的无界阻塞队列。元素需要实现 Comparable 接口或者通过构造时提供的 Comparator 进行比较。无论队列是否为空,获取操作都会立即返回最高优先级的元素。当队列为空时,获取操作将被阻塞。 |
DelayQueue | 一个使用优先级队列实现的无界阻塞队列。其中的元素必须实现 Delayed 接口,只有在其指定的延迟时间到达后才能被获取。元素按照延迟时间的长短进行排序。当队列为空时,获取操作将被阻塞。 |
SynchronousQueue | 一个不存储元素的阻塞队列。每个插入操作必须等待一个相应的移除操作,反之亦然。它在并发场景中用于线程间直接传递数据,是一种特殊的同步工具。 |
LinkedTransferQueue | 一个由链表结构组成的无界阻塞队列。它在 LinkedBlockingQueue 的基础上添加了更高级的传输操作。除了基本的 FIFO 排序外,它还支持直接传输元素给消费者。在传输操作前后,获取操作和插入操作都可以被阻塞。 |
LinkedBlockingDeque | 一个由链表结构组成的双向阻塞队列。可以在队列的两端进行插入和获取操作。它也可以在构造时指定容量,如果未指定则默认为 Integer.MAX_VALUE。当队列为空或已满时,插入和获取操作将被阻塞。 |
以上表格中列出的队列类型是 Java 中并发包(java.util.concurrent)中提供的主要队列实现。每种队列类型在不同的场景中都有特定的用途,开发者可以根据具体需求选择合适的队列来实现线程安全的数据传递和任务调度。
13.DelayQueue 的使用场景?
在很多场景我们需要用到延时任务,比如给客户异步转账操作超时后发通知告知用户,还有客户下单后多长时间内没支付则取消订单等等,这些都可以使用延时任务来实现。
- 关闭空闲连接.服务器中,有很多客户端的连接,空闲一段时间之后需要关闭之。
- 缓存.缓存中的对象,超过了空闲时间,需要从缓存中移出。
- 任务超时处理.在网络协议滑动窗口请求应答式交互时,处理超时未响应的请求。
14.TransferQueue 的使用?
使用 TransferQueue 交替打印字符串
public class Juc_03_question_AbcAbc_06 {
public static void main(String[] args){
char[] aC = "ABC".toCharArray();
char[] bC = "123".toCharArray();
TransferQueue<Character> queue = new LinkedTransferQueue<>();
new Thread(()-> {
try {
for (char c : aC){
System.out.println(queue.take());
queue.transfer(c);
}
} catch (InterruptedException e){
e.printStackTrace();
}
},"t1").start();
new Thread(()-> {
try {
for (char c : bC){
queue.transfer(c);
System.out.println(queue.take());
}
} catch (InterruptedException e){
e.printStackTrace();
}
},"t2").start();
}
}
为什么 SynchronousQueue 的吞吐量高于 LinkedBlockingQueue 和 ArrayBlockingQueue:
SynchronousQueue 无锁竞争,需要依据实际情况注意生产者线程和消费者线程的配比.
15.Fork/Join 框架原理?
Fork/Join 框架是 Java 中用于并行任务执行的一种框架,它基于"分治"(divide-and-conquer)的思想。Fork/Join 框架允许将一个大任务划分为多个小任务,然后并行地执行这些小任务,并最终将它们的结果合并起来得到最终的结果。
Fork/Join 框架的原理如下:
- 分解任务:在 Fork/Join 框架中,一个大任务会被逐步地拆分成多个小任务,直到这些小任务可以直接处理(通常是足够小到不可再拆分的大小)。这个过程称为"分解"(Forking)。
- 并行执行:一旦任务被成功地拆分成多个小任务,这些小任务就可以并行地在不同的处理器上执行。Fork/Join 框架通过工作窃取(Work-Stealing)算法来实现任务的动态调度。当一个线程完成了它所拥有的小任务后,它会尝试从其他线程的任务队列中"窃取"一个新的任务进行处理,以保持线程的高利用率。
- 合并结果:在并行执行的过程中,每个小任务都会产生一个局部结果。当所有小任务都完成后,这些局部结果将会被合并成整个大任务的最终结果。这个过程称为"合并"(Joining)。
Fork/Join 框架主要涉及以下两个关键类:
-
ForkJoinTask
:这是一个抽象类,用于表示一个可以并行执行的任务。它有两个重要的子类:RecursiveTask
用于有返回值的任务,RecursiveAction
用于没有返回值的任务。 -
ForkJoinPool
:这是 Fork/Join 框架的线程池,负责管理和调度任务的执行。它维护了一个工作队列和多个工作线程,以便高效地执行分解和合并任务。
在使用 Fork/Join 框架时,开发者需要继承RecursiveTask
或RecursiveAction
类,实现compute()
方法,在compute()
方法中将大任务分解成小任务,并实现任务的执行和结果合并逻辑。然后,将这些小任务提交给 Fork/Join 框架进行并行执行,最终得到任务的结果。Fork/Join 框架的自动任务调度和工作窃取算法能够有效地利用多核处理器的计算资源,提高并行任务执行的效率。
ForkJoinPool 由 ForkJoinTask 数组和 ForkJoinWorkerThread 数组组成, ForkJoinTask 数组负责将存放程序提交给 ForkJoinPool 的任务,而 ForkJoinWorkerThread 数组负责执行这些任务。
16.fork 方法解读
当我们调用 ForkJoinTask 的 fork 方法时,程序会调用 ForkJoinWorkerThread 的 pushTask 方法异步地执行这个任务,然后立即返回结果.代码如下。
public final ForkJoinTask<V> fork() {
Thread t;
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
((ForkJoinWorkerThread)t).workQueue.push(this);
else
ForkJoinPool.common.externalPush(this);
return this;
}
- 获取当前线程:
- 首先,代码通过
Thread.currentThread()
方法获取当前正在执行fork()
方法的线程对象,并将其赋值给变量t
。
- 判断当前线程类型:
- 然后,代码通过
instanceof
关键字判断当前线程是否是ForkJoinWorkerThread
的实例。 - 如果当前线程是
ForkJoinWorkerThread
的实例,说明该线程是 Fork/Join 框架中的工作线程。
- 提交任务:
- 如果当前线程是 Fork/Join 框架的工作线程,即
ForkJoinWorkerThread
的实例,那么代码会将当前任务this
推送(push)到该工作线程所绑定的工作队列中。 - 如果当前线程不是 Fork/Join 框架的工作线程,即是普通的 Java 线程,那么代码会通过
ForkJoinPool.common.externalPush(this)
方法将当前任务this
提交给 Fork/Join 框架的公共池(commonPool
)。
- 返回任务:
- 最后,
fork()
方法会返回当前任务this
,以便链式调用或其他后续操作。
总结:fork()
方法用于将当前任务提交给 Fork/Join 框架进行并行执行。如果当前线程是 Fork/Join 框架的工作线程,任务会被推送到该工作线程的工作队列中;如果当前线程不是 Fork/Join 框架的工作线程,任务会被提交给 Fork/Join 框架的公共池。通过fork()
方法,开发者可以将一个大任务拆分成子任务,实现任务的并行执行。
final void push(ForkJoinTask<?> task) {
ForkJoinTask<?>[] a; ForkJoinPool p;
int b = base, s = top, n;
if ((a = array) != null) { // ignore if queue removed
int m = a.length - 1; // fenced write for task visibility
U.putOrderedObject(a, ((m & s) << ASHIFT) + ABASE, task);
U.putOrderedInt(this, QTOP, s + 1);
if ((n = s - b) <= 1) {
if ((p = pool) != null)
p.signalWork(p.workQueues, this);
}
else if (n >= m)
growArray();
}
}
17.join 方法解读
Join 方法的主要作用是阻塞当前线程并等待获取结果.让我们一起看看 ForkJoinTask 的 join 方法的实现,代码如下。
public final V join() {
int s;
if ((s = doJoin() & DONE_MASK) != NORMAL)
reportException(s);
return getRawResult();
}
- 首先,代码声明一个整型局部变量
s
,用于保存任务执行的状态。 - 定义局部变量:
- 首先,代码声明一个整型局部变量
s
,用于保存任务执行的状态。
- 调用
doJoin()
方法:
- 接下来,代码调用
doJoin()
方法,该方法实际上是ForkJoinTask
类的一个抽象方法,需要在子类中实现。doJoin()
方法用于实际等待任务的完成并获取其执行状态。
- 获取执行状态:
-
doJoin()
方法返回的是任务执行状态的值。通过位运算& DONE_MASK
,将s
的值与DONE_MASK
(一个常量,表示任务状态的掩码)进行按位与运算,可以得到任务的实际执行状态。
- 判断执行状态:
- 如果执行状态
s
不等于NORMAL
(其中NORMAL
是ForkJoinTask
类中的一个常量,表示任务正常完成),则说明任务执行过程中出现了异常或被取消。在这种情况下,代码会调用reportException(s)
方法,对异常进行处理和报告。
- 返回结果:
- 如果任务的执行状态
s
等于NORMAL
,则说明任务已经正常完成。此时,代码调用getRawResult()
方法,获取任务的执行结果,并将结果返回给调用者。
总结:join()
方法用于等待当前任务的执行结果。它通过调用doJoin()
方法获取任务的执行状态,判断任务是否正常完成。如果任务正常完成,则调用getRawResult()
方法获取任务的执行结果并返回;如果任务执行过程中出现异常或被取消,则通过reportException(s)
方法对异常进行处理。通过join()
方法,可以实现对任务执行结果的获取和等待。
再来分析一下 doJoin()方法的实现代码
private int doJoin() {
int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
return (s = status) < 0 ? s :
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
(w = (wt = (ForkJoinWorkerThread)t).workQueue).
tryUnpush(this) && (s = doExec()) < 0 ? s :
wt.pool.awaitJoin(w, this, 0L) :
externalAwaitDone();
}
在 doJoin()方法里,首先通过查看任务的状态,看任务是否已经执行完成,如果执行完成,则直接返回任务状态;如果没有执行完,则从任务数组里取出任务并执行.如果任务顺利执行完成,则设置任务状态为 NORMAL,如果出现异常,则记录异常,并将任务状态设置为 EXCEPTIONAL。
18.说说 java 中的原子操作类?
常用的有 13 个原子操作类,都是通过 cas 实现的.
Java 从 JDK1.5 开始提供了 java.util.concurrent.atomic 包(以下简称 Atomic 包),这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式.因为变量的类型有很多种
- Atomic 包里一共提供了 13 个类
- 属于 4 种类型的原子更新方式
- 原子更新基本类型、
- 原子更新数组、
- 原子更新引用
- 原子更新属性(字段).
- Atomic 包里的类基本都是使用 Unsafe 实现的包装类。
atomic 提供了 3 个类用于原子更新基本类型:
- AtomicInteger 原子更新整形
- AtomicLong 原子更新长整形
- AtomicBoolean 原子更新 bool 值
atomic 里提供了三个类用于原子更新数组里面的元素:
- AtomicIntegerArray:原子更新整形数组里的元素;
- AtomicLongArray:原子更新长整形数组里的元素;
- AtomicReferenceArray:原子更新引用数组里的元素。
原子更新基本类型的 AtomicInteger 只能更新一个变量,如果要原子更新多个变量,就需要使用原子更新引用类型提供的类了.原子引用类型 atomic 包主要提供了以下几个类:
- AtomicReference:原子更新引用类型;
- AtomicReferenceFieldUpdater:原子更新引用类型里的字段;
- AtomicMarkableReference:原子更新带有标记位的引用类型.可以原子更新一个布尔类型的标记位和引用类型.构造方法是 AtomicMarkableReference(V initialRef, boolean initialMark)
如果需要原子更新某个对象的某个字段,就需要使用原子更新属性的相关类,atomic 中提供了一下几个类用于原子更新属性:
- AtomicIntegerFieldUpdater:原子更新整形属性的更新器;
- AtomicLongFieldUpdater:原子更新长整形的更新器;
- AtomicStampedReference:原子更新带有版本号的引用类型.该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
- AtomicMarkableReference 也可以解决 ABA 问题.
19.说说对 CountDownLatch 的理解?
CountDownLatch
是 Java 中并发包(java.util.concurrent)提供的一个同步工具类,它的原理主要基于倒计数的方式。CountDownLatch
允许一个或多个线程等待其他线程完成一组操作,然后再继续执行自己的任务。
CountDownLatch
的原理如下:
- 初始化计数值:
- 在创建
CountDownLatch
实例时,需要指定一个初始的计数值(count)。该计数值表示需要等待的操作的数量。
- 等待操作:
- 在主线程或某个线程中,调用
CountDownLatch
的await()
方法,该方法会使当前线程进入等待状态,直到计数值变为零。 - 如果计数值当前不为零,则
await()
方法会一直阻塞当前线程,直到计数值为零。
- 完成操作:
- 在其他线程中,执行一组操作,并在每个操作完成后,调用
CountDownLatch
的countDown()
方法。每次调用countDown()
方法,计数值减 1。
- 计数归零:
- 当
countDown()
方法的调用次数累计达到初始计数值时,计数值将变为零。
- 继续执行:
- 一旦计数值变为零,所有因调用
await()
方法而进入等待状态的线程都会被唤醒,继续执行后续的任务。
通过以上原理,CountDownLatch
可以实现线程间的协作和同步,使得某个线程等待其他线程完成特定操作后再继续执行。这对于多线程场景中需要等待其他线程完成某些初始化、数据加载或任务执行的情况非常有用。一旦计数值变为零,所有等待的线程都会被唤醒,从而实现了线程的协调和同步。
latch.await();
latch.countDown();
20.同步屏障 CyclicBarrier 理解?
CyclicBarrier
是 Java 中并发包(java.util.concurrent)提供的另一个同步工具类,它的原理是循环栅栏。CyclicBarrier
允许一组线程互相等待,直到所有线程都到达一个公共的屏障点,然后再同时继续执行。
CyclicBarrier
的原理如下:
- 初始化屏障点和参与线程数:
- 在创建
CyclicBarrier
实例时,需要指定一个屏障点(barrier),表示所有线程都要等待达到的点。同时,需要指定参与线程数(parties),表示需要等待的线程数量。
- 等待到达屏障点:
- 在各个线程中,调用
CyclicBarrier
的await()
方法,该方法会使当前线程等待,直到所有线程都到达屏障点。 - 每次调用
await()
方法,当前线程会被阻塞,直到所有参与线程都调用了await()
方法。
- 达到屏障点后继续执行:
- 一旦所有参与线程都调用了
await()
方法,它们都会在屏障点处等待。 - 当所有参与线程都到达屏障点后,
CyclicBarrier
会解除所有线程的阻塞状态,使它们可以继续执行后续的任务。
- 循环使用:
- 与
CountDownLatch
不同,CyclicBarrier
是可以循环使用的。一旦所有线程都到达屏障点并被释放,CyclicBarrier
会被重置,所有线程可以再次使用它进行下一轮的同步。
通过以上原理,CyclicBarrier
可以实现多个线程之间的协作和同步,让它们在公共的屏障点处等待,直到所有线程都到达后再同时继续执行。这对于多个线程需要同时完成某个阶段,然后再一起继续执行后续阶段的情况非常有用。每当CyclicBarrier
被重置,新的一轮同步过程又可以开始。
CyclicBarrier 依赖于 ReentrantLock 实现
barrier.await();
barrier.getNumberWaiting()
21. CyclicBarrier 和 CountDownLatch?
CyclicBarrier
和CountDownLatch
是 Java 并发包中两种不同的同步工具,它们在使用场景和原理上有一些区别。
- 同步方式:
-
CyclicBarrier
:采用循环栅栏的同步方式。多个线程在达到公共的屏障点处等待,直到所有线程都到达后才同时继续执行。CyclicBarrier
可以循环使用,每当所有线程都到达屏障点并被释放,它会被重置,可以进行下一轮的同步。 -
CountDownLatch
:采用倒计数的同步方式。一个或多个线程等待其他线程执行完成一组操作后再继续执行。CountDownLatch
的计数值在创建时被初始化,每当一个线程完成一个操作,计数值减 1,直到计数值变为零时,等待的线程被唤醒。
- 参与方面:
-
CyclicBarrier
:需要指定参与线程数,在每次等待时都要等待所有参与线程到达屏障点。 -
CountDownLatch
:需要指定倒计数值,在倒计数值变为零时所有等待的线程都会被唤醒,不需要指定参与线程数。
- 循环使用:
-
CyclicBarrier
可以循环使用,每次达到屏障点后,它会被重置,可以进行下一轮的同步。 -
CountDownLatch
在计数值减为零后,无法重置或再次使用,一旦倒计数值为零,它就失去了继续等待其他线程的能力。
- 作用场景:
-
CyclicBarrier
适用于多个线程需要等待其他线程同时到达某个屏障点后再一起继续执行的场景,常用于解决复杂任务的拆分和合并问题。 -
CountDownLatch
适用于一个或多个线程需要等待其他线程完成一组操作后再继续执行的场景,常用于线程间协调和同步。
22.说说 Semaphore ?
Semaphore
是 Java 中并发包(java.util.concurrent)提供的另一个同步工具类,它的原理基于信号量的概念。Semaphore
用于控制同时访问某个共享资源的线程数量,它维护一个许可证(permit)的计数,用于限制同时访问共享资源的线程数量。
Semaphore
的原理如下:
- 初始化许可证数量:
- 在创建
Semaphore
实例时,需要指定初始的许可证数量。这个数量表示同时允许的线程数。
- 获取许可证:
- 当一个线程需要访问共享资源时,它首先需要调用
Semaphore
的acquire()
方法。如果当前许可证数量大于零,该线程将获得一个许可证,并将许可证数量减 1。这样,许可证数量就可以反映当前可用的资源数。
- 许可证数量为零时阻塞:
- 如果当前许可证数量为零,即所有的许可证都被其他线程占用,那么
acquire()
方法将会阻塞当前线程,直到有其他线程释放许可证。
- 释放许可证:
- 当一个线程使用完共享资源后,它需要调用
Semaphore
的release()
方法来释放许可证。这将增加许可证数量,并允许其他等待许可证的线程继续执行。
通过以上原理,Semaphore
可以控制同时访问某个共享资源的线程数量,以防止过多的线程同时竞争资源导致资源过度消耗或产生冲突。Semaphore
是一种有效的并发控制工具,常用于限制同时访问共享资源的线程数量,控制并发访问的并发性。
semaphore.acquire();//阻塞
semaphore.release();//释放
intavailablePermits():返回此信号量中当前可用的许可证数。
intgetQueueLength():返回正在等待获取许可证的线程数。
booleanhasQueuedThreads():是否有线程正在等待获取许可证。
void reducePermits(intreduction):减少reduction个许可证,是个protected方法。
CollectiongetQueuedThreads):返回所有等待获取许可证的线程集合,是个protected方法。
23.CopyOnWriteArrayList 原理?
它相当于线程安全的 ArrayList。和 ArrayList 一样,它是个可变数组;但是和 ArrayList 不同的时
它具有以下特性:
- 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多于可变操作,需要在遍历期间防止线程间的冲突。
- 它是线程安全的。
- 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大。
- 迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
- 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代器时,迭代器依赖于不变的数组快照。
原理:
- CopyOnWriteArrayList 实现了 List 接口,因此它是一个队列。
- CopyOnWriteArrayList 包含了成员 lock。每一个 CopyOnWriteArrayList 都和一个监视器锁 lock 绑定,通过 lock,实现了对 CopyOnWriteArrayList 的互斥访问。
- CopyOnWriteArrayList 包含了成员 array 数组,这说明 CopyOnWriteArrayList 本质上通过数组实现的。
- CopyOnWriteArrayList 的“动态数组”机制 – 它内部有个“volatile 数组”(array)来保持数据。在“添加/修改/删除”数据时,都会新建一个数组,并将更新后的数据拷贝到新建的数组中,最后再将该数组赋值给“volatile 数组”。这就是它叫做 CopyOnWriteArrayList 的原因!CopyOnWriteArrayList 就是通过这种方式实现的动态数组;不过正由于它在“添加/修改/删除”数据时,都会新建数组,所以涉及到修改数据的操作,CopyOnWriteArrayList 效率很低;但是单单只是进行遍历查找的话,效率比较高。
- CopyOnWriteArrayList 的“线程安全”机制 – 是通过 volatile 和监视器锁 Synchrnoized 来实现的。
- CopyOnWriteArrayList 是通过“volatile 数组”来保存数据的。一个线程读取 volatile 数组时,总能看到其它线程对该 volatile 变量最后的写入;就这样,通过 volatile 提供了“读取到的数据总是最新的”这个机制的 保证。
- CopyOnWriteArrayList 通过监视器锁 Synchrnoized 来保护数据。在“添加/修改/删除”数据时,会先“获取监视器锁”,再修改完毕之后,先将数据更新到“volatile 数组”中,然后再“释放互斥锁”;这样,就达到了保护数据的目的。
24.LongAdder 原理
LongAdder 是 Java 并发包中的一个类,用于高效地支持并发计数操作。它在 Java 8 中被引入,是对 AtomicLong 的改进和优化。
在多线程环境下,通常需要对共享的计数器进行增加操作。传统的 AtomicLong 类在高并发环境下会存在性能问题,因为它使用 CAS(Compare and Swap)指令来保证操作的原子性。在高并发情况下,多个线程竞争同一个 AtomicLong 实例可能导致大量的 CAS 操作,从而降低性能。
LongAdder 通过在内部使用一种更加高效的技术,将计数分散到多个变量中,从而减少了竞争。它维护了一个数组来保存多个变量,每个线程在进行计数操作时会根据哈希算法选择一个特定的变量进行增加,而不是像 AtomicLong 那样直接竞争一个变量。
这样做的好处是,在高并发情况下,线程之间几乎没有竞争,从而减少了 CAS 操作的次数,提高了并发性能。当需要获取总计数时,LongAdder 将所有变量的值求和得到结果。
使用 LongAdder 时,你可以通过调用 add()
方法增加计数,也可以通过 sum()
方法获取当前的总计数。
以下是 LongAdder 的简单示例:
import java.util.concurrent.atomic.LongAdder;
public class LongAdderExample {
public static void main(String[] args) {
LongAdder counter = new LongAdder();
// 多个线程增加计数
Runnable incrementTask = () -> {
for (int i = 0; i < 1000; i++) {
counter.add(1);
}
};
Thread thread1 = new Thread(incrementTask);
Thread thread2 = new Thread(incrementTask);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取总计数
long total = counter.sum();
System.out.println("Total count: " + total);
}
}
总结一下,LongAdder 是在高并发环境下用于替代 AtomicLong 的一种高效并发计数器。它通过将计数分散到多个变量中,减少了线程之间的竞争,从而提高了并发性能。
觉得有用的话点个赞
👍🏻
呗。
❤️❤️❤️本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍
🔥🔥🔥Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!🌙🌙🌙