java并发高级面试题(二)
问题19:什么是StampedLock?它的作用是什么?
回答: StampedLock是java.util.concurrent.locks包中的一个类,提供了一种乐观读、写锁的机制,用于优化读多写少的场景。
StampedLock的作用是在并发读操作时使用乐观锁(tryOptimisticRead()),避免了不必要的阻塞,提高了读操作的性能。当需要进行写操作时,可以尝试升级为写锁。
示例:
问题20:什么是ForkJoinPool?它适用于什么场景?
回答: ForkJoinPool是java.util.concurrent包中的一个线程池实现,特别适用于解决分治问题(Divide and Conquer)的并行计算。它通过将大任务拆分为小任务,分配给线程池中的线程来进行并行计
算,然后将结果进行合并。
ForkJoinPool适用于需要将问题分解为多个子问题并并行求解的情况,比如递归、归并排序、MapReduce等算法。
示例:
问题26:什么是CyclicBarrier?它的作用是什么?
回答: CyclicBarrier是java.util.concurrent包中的一个同步工具,用于等待多个线程都达到一个共同的屏障点,然后再一起继续执行。它适用于多线程任务之间的同步协作,等待所有线程都完成某个阶段后再继续下一阶段。
CyclicBarrier可以被重复使用,每当所有等待线程都到达屏障点后,它会自动重置,可以继续下一轮的等待和执行。
示例:
问题27:什么是CountDownLatch?它的作用是什么?
回答: CountDownLatch是java.util.concurrent包中的一个同步工具,用于等待多个线程都完成某个任务后再继续执行。它适用于一个线程等待其他多个线程的场景,常见于主线程等待子线程完成任务。
CountDownLatch内部维护一个计数器,每个线程完成任务时会减小计数器的值,当计数器为0时,等待的线程可以继续执行。
示例:
问题28:什么是Phaser?它的作用是什么?
回答: Phaser是java.util.concurrent包中的一个同步工具,用于协调多个线程的阶段性任务。它提供了类似于CyclicBarrier和CountDownLatch的功能,但更加灵活。
Phaser支持多个阶段,每个阶段可以包含多个线程。在每个阶段结束时,所有线程都会等待,直到所有线程都到达该阶段才会继续执行。
示例:
问题29:什么是BlockingDeque?它与BlockingQueue有何不同?
回答: BlockingDeque是java.util.concurrent包中的一个接口,表示一个双端阻塞队列,即可以在队头和队尾进行插入和移除操作。与BlockingQueue相比,BlockingDeque支持更丰富的操作,例如可以在队头和队尾插入和移除元素,从队头和队尾获取元素等。
BlockingDeque的实现类包括LinkedBlockingDeque和LinkedBlockingDeque,它们可以用于实现多生产者-多消费者的并发场景。
问题30:什么是TransferQueue?它的作用是什么?
回答: TransferQueue是java.util.concurrent包中的一个接口,表示一个支持直接传输的阻塞队列。它是BlockingQueue的扩展,提供了更丰富的操作,其中最显著的是transfer()方法,该方法可以直接将元素传递给等待的消费者线程。
TransferQueue适用于一种特殊的生产者-消费者场景,其中生产者不仅可以将元素插入队列,还可以将元素直接传递给等待的消费者。
示例:
问题31:什么是ScheduledExecutorService?它的作用是什么?
回答: ScheduledExecutorService是java.util.concurrent包中的一个接口,用于支持按计划执行任务,即在指定的时间点或以固定的时间间隔执行任务。它提供了一种简单的方式来实现定时任务。
ScheduledExecutorService可以执行定时任务,如在一定延迟后执行一次,或者按照固定的时间间隔周期性地执行任务。
示例:
问题32:什么是ForkJoinTask?它的作用是什么?
回答: ForkJoinTask是java.util.concurrent包中的一个抽象类,用于表示可以被ForkJoinPool并行执行的任务。它是使用Fork-Join框架的基础。
ForkJoinTask的作用是将一个大的任务分割成更小的子任务,然后递归地并行执行这些子任务,最终将子任务的结果合并起来。它适用于需要并行处理的递归型问题,如归并排序、斐波那契数列等。
示例:
问题33:什么是CompletableFuture的组合操作?
回答: CompletableFuture支持一系列的组合操作,允许对异步任务的结果进行链式处理。这些组合操作包括:
thenApply(Function<T, U> fn): 对任务的结果进行映射转换
。thenCompose(Function<T, CompletionStage<U>> fn): 将前一个任务的结果传递给下一个任务
。thenCombine(CompletionStage<U> other, BiFunction<T, U, V> fn): 合并两个任务的结果
。thenAccept(Consumer<T> action): 对任务的结果进行消费
。thenRun(Runnable action): 在任务完成后执行一个操作
。
这些组合操作可以通过链式调用来串行执行一系列操作。
示例:
问题34:什么是ForkJoinTask的工作窃取机制?
回答: ForkJoinTask通过工作窃取(Work-Stealing)机制来实现任务的负载均衡。在ForkJoinPool中,每个线程都维护一个双端队列,存放自己的任务。当一个线程完成自己队列中的任务后,它可以从其他线程的队列中窃取任务执行,以保持线程的充分利用。
工作窃取机制能够在某些情况下避免线程因为某个任务的阻塞而空闲,从而提高了任务的并行性和效率。
问题35:ConcurrentHashMap与HashTable之间的区别是什么?
回答: ConcurrentHashMap和HashTable都是用于实现线程安全的哈希表,但它们之间有一些关键的区别:
并发度: ConcurrentHashMap支持更高的并发度,它将
哈希表分割为多个段(Segment),每个段上可以独立加锁,从而允许多个线程同时访问不同的段,降低了锁的竞争。
锁粒度: HashTable在进行操作时需要锁住整个数据结构,而ConcurrentHashMap只需要锁住某个段,使得并发性更高。
Null值: HashTable不允许键或值为null,而ConcurrentHashMap允许键和值都为null。
迭代: ConcurrentHashMap的迭代器是弱一致性的,可能会反映之前或之后的更新操作。而HashTable的迭代是强一致性的。
总的来说,如果需要更好的并发性能和更高的灵活性,通常会优先选择使用ConcurrentHashMap,而不是HashTable。
问题36:什么是Exchanger?它的作用是什么?
回答: Exchanger是java.util.concurrent包中的一个同步工具,用于在两个线程之间交换数据。每个线程调用exchange()方法后会阻塞,直到另一个线程也调用了相同的exchange()方法,然后两个线程之间交换数据。
Exchanger适用于需要在两个线程之间传递数据的场景,如一个线程生成数据,另一个线程处理数据。
示例:
问题37:BlockingQueue与Exchanger之间的区别是什么?
回答: BlockingQueue和Exchanger都是用于线程间数据传递和协作的同步工具,但它们之间有一些关键的区别:
数据传递方式: BlockingQueue通过队列的方式实现数据传递,生产者将数据放入队列,消费者从队列中取出数据。而Exchanger是通过两个线程之间直接交换数据。
协作方式: BlockingQueue适用于多生产者-多消费者场景,允许多个线程并发地插入和移除数据。Exchanger适用于两个线程之间的数据交换。
阻塞机制: BlockingQueue中的操作(如put()和take())会阻塞等待队列的状态发生变化。Exchanger中的操作(如exchange())会阻塞等待另一个线程到达。
总的来说,如果需要多个生产者和消费者之间进行数据交换,可以选择使用BlockingQueue。如果只需要两个线程之间直接交换数据,可以选择使用Exchanger。
问题38:什么是Semaphore的公平性?
回答: Semaphore提供了两种模式:公平模式和非公平模式。在公平模式下,Semaphore会按照请求许可的顺序分配许可,即等待时间最长的线程会先获得许可。在非公平模式下,许可会分配给当前可用的线程,不考虑等待的顺序。
在公平模式下,虽然保证了公平性,但可能会导致线程上下文切换的频繁发生,降低了性能。在非公平模式下,可能会出现等待时间较短的线程获取许可的情况,但性能可能会更好。
可以使用Semaphore的构造方法指定公平或非公平模式,默认情况下是非公平模式。
问题39:ConcurrentHashMap如何保证线程安全?
回答: ConcurrentHashMap使用了多种技术来保证线程安全:
分段锁: ConcurrentHashMap将内部的哈希表分成多个段(Segment),每个段上都有一个锁。不同的段可以在不同的线程上互相独立操作,减小了锁的粒度,提高了并发性能。
CAS操作: 在某些情况下,ConcurrentHashMap使用了CAS(Compare and Swap)操作,避免了使用传统的锁机制,提高了性能。
同步控制: ConcurrentHashMap使用了适当的同步控制来保证不同操作的原子性,如putIfAbsent()等。
可伸缩性: ConcurrentHashMap支持并发度的调整,可以通过调整Segment的数量来适应不同的并发级别。
以上这些技术的结合使得ConcurrentHashMap能够在高并发情况下保证线程安全。
问题40:什么是StampedLock的乐观读?
回答: StampedLock的乐观读是一种特殊的读操作,它不会阻塞其他线程的写操作,但也不会提供强一致性的保证。在乐观读期间,如果有其他线程执行了写操作,乐观读会失败。
StampedLock的乐观读通过调用tryOptimisticRead()方法开始,它会返回一个标记(stamp)。在乐观读期间,如果没有写操作发生,就可以使用这个标记来获取数据。如果乐观读之后要进行进一步的操作,可以调用validate(stamp)来检查标记是否仍然有效。
乐观读适用于读多写少的情况,可以提高读操作的性能。
问题41:什么是Semaphore?它的作用是什么?
回答: Semaphore是java.util.concurrent包中的一个同步工具,用于控制同时访问某个资源的线程数量。它通过维护一个许可数来限制线程的并发访问。
Semaphore可以用于限制同时执行某个特定操作的线程数量,或者控制同时访问某个资源(如数据库连接、文件)的线程数量。
示例:
问题42:什么是ThreadLocal?它的作用是什么?
回答: ThreadLocal是java.lang包中的一个类,用于在每个线程中存储数据副本。每个线程都可以独立地访问自己的数据副本,互不影响其他线程的数据。
ThreadLocal可以用于实现线程范围内的数据共享,每个线程可以在其中存储自己的数据,不需要显式的同步控制。它适用于需要在线程之间隔离数据的情况,如存储用户会话信息、数据库连接等。
示例:
问题43:CompletableFuture如何处理异常?
回答: CompletableFuture可以通过exceptionally和handle方法来处理异常情况。
exceptionally方法:在发生异常时,可以通过exceptionally方法提供一个处理函数,返回一个默认值或恢复操作。该处理函数只会在异常情况下被调用。
handle方法:handle方法结合了正常结果和异常情况的处理。它接收一个BiFunction,无论是正常结果还是异常,都会被传递给这个函数。
示例:
问题44:StampedLock的乐观读和悲观读有什么区别?
回答: StampedLock支持两种读模式:乐观读(Optimistic Read)和悲观读(Pessimistic Read)。
乐观读: 乐观读是一种无锁的读操作,使用tryOptimisticRead()方法可以获取一个标记(stamp),然后进行读操作。在乐观读期间,如果没有写操作发生,读取的数据是有效的。如果后续要对数据进行写操作,需要使用validate(stamp)方法来验证标记是否仍然有效。
悲观读: 悲观读是一种使用读锁的读操作,使用readLock()方法来获取读锁,保证在读操作期间不会被写操作所影响。
乐观读适用于读多写少的情况,悲观读适用于读写并发较高的情况。
问题45:CountDownLatch与CyclicBarrier之间的区别是什么?
回答: CountDownLatch和CyclicBarrier都是用于协调多个线程之间的同步,但它们之间有一些关键的区别:
使用场景: CountDownLatch用于等待多个线程完成某个任务,然后继续执行。CyclicBarrier用于等待多个线程都达到一个共同的屏障点,然后再一起继续执行。
重用性: CountDownLatch的计数器只能使用一次,一旦计数器归零,就不能再使用。CyclicBarrier可以被重复使用,每次都会自动重置。
等待机制: CountDownLatch使用await()方法等待计
数器归零。CyclicBarrier使用await()方法等待所有线程到达屏障点。
线程数量: CountDownLatch的计数器数量固定。CyclicBarrier的屏障点数量由用户指定。
总的来说,如果需要等待多个线程都完成某个任务后再继续执行,可以选择使用CountDownLatch。如果需要等待多个线程都达到一个共同的屏障点再一起继续执行,可以选择使用CyclicBarrier。
问题46:Semaphore和ReentrantLock之间的区别是什么?
回答: Semaphore和ReentrantLock都是java.util.concurrent包中用于线程同步的工具,但它们之间有一些区别:
用途: Semaphore用于控制同时访问某个资源的线程数量,而ReentrantLock用于提供独占锁功能,即只有一个线程可以获取锁并访问受保护的资源。
锁的类型: Semaphore不是一种锁,而是一种信号量机制。ReentrantLock是一种显式的独占锁。
并发度: Semaphore可以同时允许多个线程访问受保护资源,具有更高的并发度。ReentrantLock在同一时刻只允许一个线程访问受保护资源。
阻塞机制: Semaphore使用许可机制来控制访问,当没有许可时,线程会阻塞等待。ReentrantLock使用可重入锁,线程可以重复获得锁,但需要相应数量的解锁操作。
应用场景: Semaphore适用于资源池管理、限流等场景。ReentrantLock适用于更加复杂的同步需求,可以控制锁的获取和释放,提供更多灵活性。
问题47:Semaphore的公平性与非公平性有什么区别?
回答: Semaphore可以使用公平模式和非公平模式。
公平模式: 在公平模式下,Semaphore会按照线程请求许可的顺序分配许可,即等待时间最长的线程会先获得许可。公平模式保证了线程的公平竞争,但可能会导致线程上下文切换频繁。
非公平模式: 在非公平模式下,Semaphore不会考虑线程的等待时间,许可会分配给当前可用的线程。非公平模式可能会导致等待时间较短的线程优先获得许可,但性能可能会更好。
可以通过Semaphore的构造方法来指定使用公平模式还是非公平模式,默认情况下是非公平模式。
问题48:ReentrantReadWriteLock是什么?它的作用是什么?
回答: ReentrantReadWriteLock是java.util.concurrent包中的一个锁实现,用于解决读写锁问题。它允许多个线程同时进行读操作,但在进行写操作时只允许一个线程。
ReentrantReadWriteLock由一个读锁和一个写锁组成。读锁允许多个线程同时获得锁进行读操作,写锁只允许一个线程获得锁进行写操作。
ReentrantReadWriteLock适用于读多写少的场景,可以提高并发性能。
示例:
问题49:Phaser和CyclicBarrier之间的区别是什么?
回答: Phaser和CyclicBarrier都是用于多线程之间的协调和同步,但它们之间有一些区别:
屏障点数量: CyclicBarrier的屏障点数量是在创建时指定的,且在初始化之后不能更改。而Phaser的屏障点数量可以在任何时候进行动态修改。
注册线程数: Phaser允许动态注册和注销参与者线程,可以动态地控制协调的线程数量。而CyclicBarrier一旦创建,线程数量是固定的。
阶段(Phase): Phaser支持多个阶段,每个阶段可以有不同的参与者数量。每次进入新阶段时,Phaser会重新计数。
回调功能: Phaser支持在每个阶段的入口和出口设置回调函数,用于执行特定操作。
总的来说,如果需要更灵活地控制线程数量和阶段,以及支持动态的参与者注册和注销,可以选择使用Phaser。如果只需要等待多个线程都达到一个共同的屏障点再一起继续执行,可以选择使用CyclicBarrier。
问题50:Exchanger和TransferQueue之间的区别是什么?
回答: Exchanger和TransferQueue都是用于线程之间的数据交换,但它们之间有一些区别
:
交换方式: Exchanger是一种简单的同步工具,只允许两个线程之间交换数据。TransferQueue是一个更高级的接口,支持多个线程之间的数据传递。
用途: Exchanger用于在两个线程之间交换数据,每个线程等待对方。TransferQueue用于实现生产者-消费者模型,支持多个生产者和消费者,可以在队列中传递数据。
特性: Exchanger只提供数据交换功能,不涉及其他操作。TransferQueue提供了更丰富的队列操作,如put()、take()等。
实现: Exchanger的实现是基于Lock和Condition等基本同步工具。TransferQueue的实现通常基于链表等数据结构,同时也使用了Lock等同步机制。
总的来说,如果需要简单的两个线程之间的数据交换,可以选择使用Exchanger。如果需要实现更复杂的生产者-消费者模型,可以选择使用TransferQueue的实现类,如LinkedTransferQueue。
问题51:BlockingQueue和TransferQueue之间的区别是什么?
回答: BlockingQueue和TransferQueue都是java.util.concurrent包中的接口,用于实现生产者-消费者模型,但它们之间有一些区别:
数据传递方式: BlockingQueue使用队列的方式进行数据传递,生产者将数据放入队列,消费者从队列中取出数据。TransferQueue也使用队列的方式进行数据传递,但具有更多特性,如阻塞等待生产者或消费者就绪。
特性: BlockingQueue提供了多种阻塞等待的方法,如put()、take()等。TransferQueue在BlockingQueue的基础上增加了更丰富的特性,如tryTransfer()、hasWaitingConsumer()等,使得生产者-消费者模型更灵活。
等待机制: BlockingQueue中的操作(如put()和take())会阻塞等待队列的状态发生变化。TransferQueue中的操作(如transfer()和take())也会阻塞等待,但可以等待生产者或消费者就绪。
总的来说,TransferQueue是BlockingQueue的扩展,提供了更丰富的特性,适用于更灵活的生产者-消费者模型。
问题52:CompletableFuture和Future之间的区别是什么?
回答: CompletableFuture和Future都是用于异步编程的接口,但它们之间有一些区别:
是否可编排: CompletableFuture支持编排多个异步操作,可以通过一系列的方法链来组合多个操作,使得异步操作更具可读性。Future不支持直接的方法链编排。
回调机制: CompletableFuture支持添加回调函数,可以在异步操作完成后执行指定的操作。Future本身不支持回调机制,但可以通过轮询的方式来检查异步操作的完成状态。
异常处理: CompletableFuture支持更灵活的异常处理,可以通过handle()、exceptionally()等方法来处理异常情况。Future的异常处理相对有限,需要在调用get()方法时捕获ExecutionException。
异步操作结果: CompletableFuture的方法可以返回新的CompletableFuture,使得异步操作的结果可以被后续的操作使用。Future的结果通常需要通过get()方法获取,且不支持链式操作。
总的来说,CompletableFuture提供了更强大的异步编程能力,更灵活的异常处理和编排机制,使得异步操作更加简洁和可读。
问题53:Semaphore和Mutex之间有什么区别?
回答: Semaphore和Mutex(互斥锁)都是用于实现线程同步的机制,但它们之间有一些区别:
用途: Semaphore用于控制同时访问某个资源的线程数量,可以允许多个线程同时访问。Mutex用于保护临界区资源,同时只允许一个线程访问。
线程数: Semaphore可以同时允许多个线程访问受保护资源,其许可数可以设置。Mutex只允许一个线程获得锁。
操作: Semaphore的主要操作是acquire()和release(),分别用于获取和释放许可。Mutex的操作是获取和释放锁,通常使用lock()和unlock()。
应用场景: Semaphore适用于资源池管理、限流等场景,需要控制并发访问的线程数量。Mutex适用于需要保护临界区资源,防止并发访问造成数据不一致的场景。
总的来说,Semaphore更多地用于控制并发访问的线程数量,而Mutex更多地用于保护共享资源的完整性。
问题54:ReadWriteLock和StampedLock之间的区别是什么?
回答: ReadWriteLock和StampedLock都是java.util.concurrent包中用于实现读写锁的机制,但它们之间有一些区别:
支持的模式: ReadWriteLock支持经典的读锁和写锁模式,允许多个线程同时获得读锁,但在写锁模式下只允许一个线程获得锁。StampedLock除了支持读锁和写锁模式外,还支持乐观读模式。
乐观读: StampedLock支持乐观读,允许在读锁的基础上进行无锁读操作,但可能会失败。ReadWriteLock不支持乐观读。
性能: StampedLock的乐观读操作具有更低的开销,适用于读多写少的情况。ReadWriteLock适用于读写操作相对均衡的情况。
应用场景: ReadWriteLock适用于需要高并发的读操作场景,如缓存。StampedLock适用于读多写少且对性能有较高要求的场景,可以使用乐观读提高性能。
总的来说,如果需要更细粒度的读写控制和支持乐观读模式,可以选择使用StampedLock。如果只需要传统的读写锁模式,可以选择使用ReadWriteLock。
问题55:BlockingQueue和SynchronousQueue之间的区别是什么?
回答: BlockingQueue和SynchronousQueue都是java.util.concurrent包中的队列,但它们之间有一些区别:
队列特性: BlockingQueue是一个允许在队列为空或满时进行阻塞等待的队列,支持多个生产者和消费者。SynchronousQueue是一个特殊的队列,它不存储元素,每个插入操作必须等待一个对应的删除操作,反之亦然。
存储元素: BlockingQueue可以存储多个元素,具有一定的容量。SynchronousQueue不存储元素,每次插入操作需要等待相应的删除操作。
用途: BlockingQueue适用于生产者-消费者模型,允许在队列满或空时进行合理的等待。SynchronousQueue适用于一对一的线程通信,生产者必须等待消费者消费。
性能: SynchronousQueue的性能相对较高,因为它不需要存储元素,只是进行传递。
总的来说,BlockingQueue适用于多生产者-多消费者的场景,需要在队列满或空时进行等待。SynchronousQueue适用于一对一的线程通信,具有更高的性能。
问题56:CopyOnWriteArrayList和ArrayList之间的区别是什么?
回答: CopyOnWriteArrayList和ArrayList都是java.util.concurrent包中的列表,但它们之间有一些区别:
并发性: ArrayList不是线程安全的,多个线程同时进行读写操作可能导致数据不一致。CopyOnWriteArrayList是线程安全的,可以在多线程环境下进行读写操作。
写操作开销: ArrayList在写操作(如添加、删除元素)时需要进行显式的同步控制,可能引起线程阻塞。CopyOnWriteArrayList通过复制整个数据结构来实现写操作,不会阻塞正在进行的读操作,但会引起写操作的开销。
迭代器: ArrayList的迭代器不支持并发修改,可能会抛出ConcurrentModificationException异常。CopyOnWriteArrayList的迭代器支持并发修改,可以在迭代的同时进行修改。
适用场景: ArrayList适用于单线程环境或只读操作的多线程环境。CopyOnWriteArrayList适用于读多写少的多线程环境,适合在遍历操作频繁、写操作较少的情况下使用。
总的来说,如果需要在多线程环境中进行读写操作,可以选择使用CopyOnWriteArrayList,以保证线程安全性。如果在单线程环境或只读操作的多线程环境下使用,可以选择使用ArrayList。
问题57:ForkJoinPool是什么?它的作用是什么?
回答: ForkJoinPool是java.util.concurrent包中的一个线程池实现,专门用于支持分治任务的并行处理。它的主要作用是高效地执行可以被拆分为子任务并行执行的任务,将大任务拆分为小任务,然后将子任务的结果合并。
ForkJoinPool使用工作窃取(Work-Stealing)算法,即空闲线程从其他线程的任务队列中窃取任务来执行。这可以减少线程等待时间,提高并行处理效率。
ForkJoinPool通常用于解决递归、分治、MapReduce等问题,如并行排序、矩阵乘法等。
示例:
在上面的示例中,我们使用ForkJoinPool执行了一个求和任务,将大数组拆分为子任务并行执行,然后合并结果。这展示了ForkJoinPool的用法和分治任务的处理方式。
问题58:CompletableFuture的thenCompose和thenCombine有什么区别?
回答: thenCompose和thenCombine都是CompletableFuture的组合方法,用于处理异步操作的结果。它们之间的区别
在于:
thenCompose: thenCompose方法用于将一个异步操作的结果传递给另一个异步操作,并返回一个新的CompletableFuture。第二个操作的返回值是一个CompletionStage,通过thenCompose可以将两个操作串联起来,实现链式的异步操作。
thenCombine: thenCombine方法用于组合两个独立的异步操作的结果,然后对这两个结果进行处理,并返回一个新的CompletableFuture。它接受一个BiFunction参数,用于将两个结果进行合并。
示例:
在上面的示例中,我们展示了thenCombine和thenCompose的用法。thenCombine用于组合两个独立的结果,而thenCompose用于串联两个操作的结果。
最后更新于