并发工具类
CountDownLatch 了解吗
CountDownLatch 是 JUC 中的一个同步工具类,用于协调多个线程之间的同步,确保主线程在多个子线程完成任务后继续执行。
它的核心思想是通过一个倒计时计数器来控制多个线程的执行顺序。
在使用的时候,我们需要先初始化一个 CountDownLatch 对象,指定一个计数器的初始值,表示需要等待的线程数量。
然后在每个子线程执行完任务后,调用 countDown() 方法,计数器减 1。
接着主线程调用 await() 方法进入阻塞状态,直到计数器为 0,也就是所有子线程都执行完任务后,主线程才会继续执行。
假如要查10万多条数据,用线程池分成20个线程去执行,怎么做到等所有的线程都查找完之后,即最后一条结果查找结束了,才输出结果?
很简单,可以使用 CountDownLatch 来实现。CountDownLatch 非常适合这个场景。
第一步,创建 CountDownLatch 对象,初始值设定为 20,表示 20 个线程需要完成任务。
第二步,创建线程池,每个线程执行查询操作,查询完毕后调用 countDown() 方法,计数器减 1。
第三步,主线程调用 await() 方法,等待所有线程执行完毕。
CyclicBarrier 了解吗?
CyclicBarrier 的字面意思是可循环使用的屏障,用于多个线程相互等待,直到所有线程都到达屏障后再同时执行。
在使用的时候,我们需要先初始化一个 CyclicBarrier 对象,指定一个屏障值 N,表示需要等待的线程数量。
然后每个线程执行 await() 方法,表示自己已经到达屏障,等待其他线程,此时屏障值会减 1。
当所有线程都到达屏障后,也就是屏障值为 0 时,所有线程会继续执行。
Semaphore 了解吗?
Semaphore——信号量,用于控制同时访问某个资源的线程数量,类似限流器,确保最多只有指定数量的线程能够访问某个资源,超过的必须等待。
Exchanger 了解吗?
Exchanger——交换者,用于在两个线程之间进行数据交换。
支持双向数据交换,比如说线程 A 调用 exchange(dataA),线程 B 调用 exchange(dataB),它们会在同步点交换数据,即 A 得到 B 的数据,B 得到 A 的数据。
如果一个线程先调用 exchange(),它会阻塞等待,直到另一个线程也调用 exchange()。
使用 Exchanger 的时候,需要先创建一个 Exchanger 对象,然后在两个线程中调用 exchange() 方法,就可以进行数据交换了。
Exchanger 可以用于遗传算法,也可以用于校对工作,比如我们将纸制银行流水通过人工的方式录入到电子银行时,为了避免错误,可以录入两遍,然后通过 Exchanger 来校对两次录入的结果。
能说一下 ConcurrentHashMap 的实现吗?
好的。ConcurrentHashMap 是 HashMap 的线程安全版本。
JDK 7 采用的是分段锁,整个 Map 会被分为若干段,每个段都可以独立加锁。不同的线程可以同时操作不同的段,从而实现并发。
JDK 8 使用了一种更加细粒度的锁——桶锁,再配合 CAS + synchronized 代码块控制并发写入,以最大程度减少锁的竞争。
对于读操作,ConcurrentHashMap 使用了 volatile 变量来保证内存可见性。
对于写操作,ConcurrentHashMap 优先使用 CAS 尝试插入,如果成功就直接返回;否则使用 synchronized 代码块进行加锁处理。
说一下 JDK 7 中 ConcurrentHashMap 的实现原理?
好的。
JDK 7 的 ConcurrentHashMap 采用的是分段锁,整个 Map 会被分为若干段,每个段都可以独立加锁,每个段类似一个 Hashtable。
每个段维护一个键值对数组 HashEntry<K, V>[] table,HashEntry 是一个单项链表。
段继承了 ReentrantLock,所以每个段都是一个可重入锁,不同的线程可以同时操作不同的段,从而实现并发。
说一下 JDK 7 中 ConcurrentHashMap 的 put 流程?
put 流程和 HashMap 非常类似,只不过是先定位到具体的段,再通过 ReentrantLock 去操作而已。一共可以分为 4 个步骤:
第一步,计算 key 的 hash,定位到段,段如果是空就先初始化;
第二步,使用 ReentrantLock 进行加锁,如果加锁失败就自旋,自旋超过次数就阻塞,保证一定能获取到锁;
第三步,遍历段中的键值对 HashEntry,key 相同直接替换,key 不存在就插入。
第四步,释放锁。
说一下 JDK 7 中 ConcurrentHashMap 的 get 流程?
get 就更简单了,先计算 key 的 hash 找到段,再遍历段中的键值对,找到就直接返回 value。
get 不用加锁,因为是 value 是 volatile 的,所以线程读取 value 时不会出现可见性问题。
说一下 JDK 8 中 ConcurrentHashMap 的实现原理?
好的。
JDK 8 中的 ConcurrentHashMap 取消了分段锁,采用 CAS + synchronized 来实现更细粒度的桶锁,并且使用红黑树来优化链表以提高哈希冲突时的查询效率,性能比 JDK 7 有了很大的提升。
说一下 JDK 8 中 ConcurrentHashMap 的 put 流程?
第一步,计算 key 的 hash,以确定桶在数组中的位置。如果数组为空,采用 CAS 的方式初始化,以确保只有一个线程在初始化数组。
第二步,如果桶为空,直接 CAS 插入节点。如果 CAS 操作失败,会退化为 synchronized 代码块来插入节点。
插入的过程中会判断桶的哈希是否小于 0(f.hash >= 0),小于 0 说明是红黑树,大于等于 0 说明是链表。
这里补充一点:在 ConcurrentHashMap 的实现中,红黑树节点 TreeBin 的 hash 值固定为 -2。
第三步,如果链表长度超过 8,转换为红黑树。
第四步,在插入新节点后,会调用 addCount() 方法检查是否需要扩容。
说一下 JDK 8 中 ConcurrentHashMap 的 get 流程?
get 也是通过 key 的 hash 进行定位,如果该位置节点的哈希匹配且键相等,则直接返回值。
如果节点的哈希为负数,说明是个特殊节点,比如说如树节点或者正在迁移的节点,就调用find方法查找。
否则遍历链表查找匹配的键。如果都没找到,返回 null。
为什么 ConcurrentHashMap 在 JDK 1.7 中要用 ReentrantLock,而在 JDK 1.8 要用 synchronized
JDK 1.7 中的 ConcurrentHashMap 使用了分段锁机制,每个 Segment 都继承了 ReentrantLock,这样可以保证每个 Segment 都可以独立地加锁。
而在 JDK 1.8 中,ConcurrentHashMap 取消了 Segment 分段锁,采用了更加精细化的锁——桶锁,以及 CAS 无锁算法,每个桶都可以独立地加锁,只有在 CAS 失败时才会使用 synchronized 代码块加锁,这样可以减少锁的竞争,提高并发性能。
ConcurrentHashMap 怎么保证可见性?
ConcurrentHashMap 中的 Node 节点中,value 和 next 都是 volatile 的,这样就可以保证对 value 或 next 的更新会被其他线程立即看到。
为什么 ConcurrentHashMap 比 Hashtable 效率高
Hashtable 在任何时刻只允许一个线程访问整个 Map,是通过对整个 Map 加锁来实现线程安全的。比如 get 和 put 方法,是直接在方法上加的 synchronized 关
而 ConcurrentHashMap 在 JDK 8 中是采用 CAS + synchronized 实现的,仅在必要时加锁。
比如说 put 的时候优先使用 CAS 尝试插入,如果失败再使用 synchronized 代码块加锁。
get 的时候是完全无锁的,因为 value 是 volatile 变量 修饰的,保证了内存可见性。
能说一下 CopyOnWriteArrayList 的实现原理吗?
CopyOnWriteArrayList 是 ArrayList 的线程安全版本,适用于读多写少的场景。
CopyOnWrite 的核心思想是写操作时创建一个新数组,修改后再替换原数组,这样就能够确保读操作无锁,从而提高并发性能。
内部使用 volatile 变量来修饰数组 array,以确保读操作的内存可见性。
写操作的时候使用 ReentrantLock 来保证线程安全。
缺点就是写操作的时候会复制一个新数组,如果数组很大,写操作的性能会受到影响。
能说一下 BlockingQueue 吗?
BlockingQueue 是 JUC 包下的一个线程安全队列,支持阻塞式的“生产者-消费者”模型。
当队列容器已满,生产者线程会被阻塞,直到消费者线程取走元素后为止;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。
BlockingQueue 的实现类有很多,比如说 ArrayBlockingQueue、PriorityBlockingQueue 等。
阻塞队列是如何实现的?
阻塞队列使用 ReentrantLock + Condition 来确保并发安全。
以 ArrayBlockingQueue 为例,它内部维护了一个数组,使用两个指针分别指向队头和队尾。
put 的时候先用 ReentrantLock 加锁,然后判断队列是否已满,如果已满就阻塞等待,否则插入元素。
