面试难度:★★★★
考察概率:★★★
#莫等闲,白了少年头#
本人从毕业开始一直在一线互联网大厂工作,现任技术TL,出版过《深入理解Java并发》一书,折腾过技术开源项目,并长期作为面试官参与面试,深谙双方的诉求与技术沟通。如今归零心态,再出发。#莫等闲,白了少年头#
技术交流+v:xxxyxsyy1234(一起努力,每日打卡) 2000+以面试官视角总结的考点,可与我共同打卡学习
面试官视角
对常用的并发工具类的掌握是个比较基础的考察点,只有掌握j.u.c本身提供的工具类,在日常的业务开发中才能事半功倍的效果。但是这部分对原理的考察还是会相对而言较少的,默认是在前面的原理考察部分过关后,常用的几个并发工具类只是基于原理而搭建的并发组件。对候选人而言,能够知道每种工具类的使用场景即可。
面试题
- AtomicIntegerArray 和 AtomicInteger[]有什么区别?
- LongAdder和AtomicLong的有什么区别?
- longAccumulate中Cell数组的初始大小为何是2?
- CyclicBarrier 为什么要使用 ReentrantLock?可以使用CAS吗?
- CyclicBarrier 和CountDownLatch的区别是什么?
回答要点
1. AtomicIntegerArray 和 AtomicInteger[]有什么区别?
AtomicIntegerArray和AtomicInteger[]都是Java中用于原子操作的类,它们有以下区别:
- 数据结构:AtomicIntegerArray是一个以数组形式存储多个原子整型值的类,其中的每个元素都是原子的。而AtomicInteger[]是一个存储多个AtomicInteger对象的数组,每个AtomicInteger对象都是原子的。
- 功能:AtomicIntegerArray提供了一系列原子操作方法,用于对整个数组或指定索引位置的元素进行原子操作,如get、set、getAndSet、addAndGet、compareAndSet等。而AtomicInteger[]只是一个普通的AtomicInteger对象数组,它没有提供特定的原子操作方法,需要通过操作数组中的每个AtomicInteger对象来实现原子操作。
- 灵活性:由于AtomicIntegerArray是以数组形式存储的,因此可以通过数组的索引来直接访问和操作特定位置的元素,操作更加灵活。而AtomicInteger[]需要通过数组索引获取AtomicInteger对象,然后再对该对象进行操作,操作相对繁琐。
- 内存占用:AtomicIntegerArray在内存上比AtomicInteger[]更加节省空间。因为AtomicIntegerArray只存储原子整型值,而AtomicInteger[]需要额外存储多个AtomicInteger对象的引用。
综上所述,AtomicIntegerArray适用于需要对整个数组或指定位置的元素进行原子操作的场景,它提供了一系列针对数组操作的原子方法。而AtomicInteger[]适用于需要对多个AtomicInteger对象进行原子操作的场景,通过操作数组中的每个AtomicInteger对象来实现原子操作。
2. LongAdder和AtomicLong的有什么区别?
LongAdder和AtomicLong都是用于原子操作的类,它们有以下区别:
- 内部实现:AtomicLong使用CAS(Compare and Swap)操作来实现原子性,而LongAdder则使用分段锁(Striped Long Adder)来实现原子性。
- 并发性能:在高并发情况下,LongAdder的性能通常优于AtomicLong。这是因为LongAdder在多线程环境下将竞争分散到多个单元(Cell)中,减少了竞争,从而提高了吞吐量。而AtomicLong在高并发情况下可能会发生激烈的竞争,导致性能下降。
- 内存占用:LongAdder在内存上比AtomicLong占用更多空间。LongAdder通过使用多个单元(Cell)来分散竞争,每个单元都是一个独立的变量,需要额外的内存来存储。而AtomicLong只需要存储一个long类型的值。
- 计数操作:对于计数操作(如add()、increment()),AtomicLong和LongAdder的用法相似,都可以实现原子的增减操作。不过LongAdder提供了更高效的计数操作,在高并发环境下,多线程可以独立地更新不同的单元,而不会产生线程间的竞争。
综上所述,AtomicLong适用于单线程或低并发情况下的原子计数操作,它具有较低的内存占用。而LongAdder适用于高并发情况下的原子计数操作,通过分段锁的方式减少了竞争,提供了更好的性能。因此,根据具体的使用场景和需求,选择适合的类进行原子计数操作。
3. longAccumulate中Cell数组的初始大小为何是2?
在LongAdder源代码中,Cell数组的初始大小为2是为了在一开始就提供一定程度的并发度。在LongAdder中,每个线程会通过哈希算法将计数值加到不同的Cell中,这样可以减少线程之间的竞争,提高并发性能。
初始大小为2是一个经验性的选择,它可以在大多数情况下提供适当的并发度。当线程数较少时,每个线程都可以在不同的Cell中进行计数,避免了线程之间的竞争。如果并发度不足,随着线程数量的增加,LongAdder会动态地增加Cell的数量,以提供更多的并发度。
4. CyclicBarrier 为什么要使用 ReentrantLock?可以使用CAS吗?
回答这个问题。首先要回答一下ReentrantLock和CAS是什么关系。
ReentrantLock是Java中的一个可重入锁实现,它使用了CAS(Compare and Swap)操作来实现对锁状态的获取和释放。
CAS是一种乐观锁技术,它通过比较内存中的值与预期值,如果相等则将新值写入内存,否则不进行操作。在并发环境下,CAS可以实现非阻塞的原子性操作,避免了传统互斥锁带来的线程阻塞和上下文切换开销。
ReentrantLock底层使用了CAS操作来实现对锁状态的获取和释放。具体来说,ReentrantLock内部维护了一个volatile变量来表示锁的状态,通常是一个整型值。当线程尝试获取锁时,它会使用CAS操作将锁的状态从未锁定改变为锁定状态,如果CAS操作成功,表示当前线程成功获取到锁;如果CAS操作失败,表示有其他线程持有锁,当前线程就会进入等待状态。
ReentrantLock还提供了可重入性的特性,即同一个线程可以多次获取同一个锁而不会产生死锁。这是通过在ReentrantLock内部维护一个线程的持有计数器实现的。当线程再次获取已经持有的锁时,计数器会递增,只有计数器变为0时才会真正释放锁。
因此,ReentrantLock与CAS的关系是,ReentrantLock利用CAS操作来实现对锁状态的获取和释放,通过CAS的原子性保证锁的正确性和并发安全性。CAS操作是ReentrantLock实现中的关键技术之一,使得ReentrantLock能够提供可重入性和高效的并发控制。
所以CAS只是一种乐观锁技术,ReentrantLock的功能更加强大。CyclicBarrier直接使用ReentrantLock会事半功倍。
5. CyclicBarrier 和CountDownLatch的区别是什么?
CyclicBarrier和CountDownLatch是Java并发包中提供的线程同步工具,它们有一些相似之处,但也存在一些重要的区别。
- 同步方式不同:
- CyclicBarrier:CyclicBarrier是一种可重用的同步工具,它允许一组线程在达到某个同步点时相互等待,然后同时继续执行。线程通过调用await()方法等待其他线程到达同步点。
- CountDownLatch:CountDownLatch是一种单次使用的同步工具,它允许一个或多个线程等待其他线程执行完成后再继续执行。线程通过调用await()方法等待计数器达到零。
- 计数器的作用不同:
- CyclicBarrier:CyclicBarrier使用一个计数器来记录需要等待的线程数量,每个线程到达同步点时调用await()方法,计数器会减1。当计数器减到0时,所有等待的线程会被释放,继续执行后续操作。
- CountDownLatch:CountDownLatch也使用一个计数器,但其作用是进行倒数计数。初始时,计数器被设置为等待的线程数量。每个线程完成任务后调用countDown()方法,计数器会减1。当计数器减到0时,等待的线程会被释放。
- 可重用性不同:
- CyclicBarrier:CyclicBarrier是可重用的,计数器可以被重置并再次使用。当所有线程到达同步点后,计数器会被重置为初始值,可以继续使用。
- CountDownLatch:CountDownLatch是一次性的,一旦计数器减到0,就不能再次使用。
综上所述,CyclicBarrier适用于一组线程相互等待并互相配合的场景,而CountDownLatch适用于一个或多个线程等待其他一组线程的操作完成的场景。一般情况下2个工具可以相互替换。
代码考核
对于并发工具而言,在面试中很少去考察原理之类的,更多的是考察几个组件间的使用场景。对社招而言来说,偶尔会出一个场景让候选人去聊聊具体的实现思路。对校招而言,我之前校招手撕过CountDownLatch的代码,也是出了一个场景,使用CountDownLatch去实现。
知识点详情
这部分可以参考本人的书籍《深入理解Java并发》,或者本人博客