【面试】Java并发篇(二)

0、问题大纲

二、JUC工具、线程池

2.1 JUC包

1、Java并发包提供了哪些并发工具类?【第19讲】

2、如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全? 【第10讲】
 - 追问1:HashMap/HashTable/ConcurrentHashMap结构,底层(*4),如何保证线程安全,怎么实现(*3- 追问2:ConcurrentHashMap中1.71.8的区别(*23、并发包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别?【第20讲】

2.2 线程池

1、为什么用线程池,有何好处?(*2)

2、Java并发类库提供的线程池有哪几种? 分别有什么特点?【第21讲】
 - 追问1:几种队列(*3),分别作用(*2- 追问2:排队策略有哪些?
 - 追问3:线程池的(核心)参数(*4- 追问4:如果任务数超过的核心线程数,会发生什么?

二、JUC工具、线程池

2.1 JUC包

1、Java并发包提供了哪些并发工具类?【第19讲】

我们通常所说的并发包也就是 java.util.concurrent 及其子包,集中了 Java 并发的各种基础工具类,具体主要包括几个方面:

提供了比 synchronized 更加高级的各种同步结构,包括 CountDownLatch、CyclicBarrier、Semaphore 等,可以实现更加丰富的多线程操作,比如利用 Semaphore 作为资源控制器,限制同时进行工作的线程数量。

各种线程安全的容器,比如最常见的 ConcurrentHashMap、有序的 ConcurrentSkipListMap,或者通过类似快照机制,实现线程安全的动态数组 CopyOnWriteArrayList 等。

各种并发队列实现,如各种 BlockingQueue 实现,比较典型的 ArrayBlockingQueue、 SynchronousQueue 或针对特定场景的 PriorityBlockingQueue 等。

强大的 Executor 框架,可以创建各种不同类型的线程池,调度任务运行等,绝大部分情况下,不再需要自己从头实现线程池和任务调度器。

2、如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全? 【第10讲】

Java 提供了不同层面的线程安全支持。在传统集合框架内部,除了 Hashtable 等同步容器,还提供了所谓的同步包装器(Synchronized Wrapper),我们可以调用 Collections 工具类提供的包装方法,来获取一个同步的包装容器(如 Collections.synchronizedMap),但是它们都是利用非常粗粒度的同步方式,在高并发情况下,性能比较低下。

另外,更加普遍的选择是利用并发包提供的线程安全容器类,它提供了:各种并发容器,比如 ConcurrentHashMap、CopyOnWriteArrayList。
各种线程安全队列(Queue/Deque),如 ArrayBlockingQueue、SynchronousQueue。
各种有序容器的线程安全版本等。

具体保证线程安全的方式,包括有从简单的 synchronize 方式,到基于更加精细化的,比如基于分离锁实现的 ConcurrentHashMap 等并发实现等。具体选择要看开发的场景需求,总体来说,并发包内提供的容器通用场景,远优于早期的简单同步实现。

追问1:HashMap/HashTable/ConcurrentHashMap结构,底层(*4),如何保证线程安全,怎么实现(*3)

……

追问2:ConcurrentHashMap中1.7和1.8的区别(*2)

……

3、并发包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别?【第20讲】

有时候我们把并发包下面的所有容器都习惯叫作并发容器,但是严格来讲,类似 ConcurrentLinkedQueue 这种“Concurrent*”容器,才是真正代表并发。

关于问题中它们的区别:

  • Concurrent 类型基于 lock-free,在常见的多线程访问场景,一般可以提供较高吞吐量。
  • 而 LinkedBlockingQueue 内部则是基于锁,并提供了 BlockingQueue 的等待性方法。

不知道你有没有注意到,java.util.concurrent 包提供的容器(Queue、List、Set)、Map,从命名上可以大概区分为 Concurrent*、CopyOnWrite和 Blocking等三类,同样是线程安全容器,可以简单认为:

  • Concurrent 类型没有类似 CopyOnWrite 之类容器相对较重的修改开销。
  • 但是,凡事都是有代价的,Concurrent 往往提供了较低的遍历一致性。你可以这样理解所谓的弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历。
  • 与弱一致性对应的,就是我介绍过的同步容器常见的行为“fail-fast”,也就是检测到容器在遍历过程中发生了修改,则抛出 ConcurrentModificationException,不再继续遍历。
  • 弱一致性的另外一个体现是,size 等操作准确性是有限的,未必是 100% 准确。
  • 与此同时,读取的性能具有一定的不确定性。

2.2 线程池

1、为什么用线程池,有何好处?(*2)

线程是不能够重复启动的,创建或销毁线程存在一定的开销,线程池能够创建一定空闲线程,任务到来,会选择空闲线程处理,处理完不退出,等待下一次,当大部分线程阻塞时会自动销毁一部分线程,回收系统资源。简言之,线程池技术能提高系统资源利用效率,简化线程管理。

2、Java并发类库提供的线程池有哪几种? 分别有什么特点?【第21讲】

开发者利用 Executors 提供的通用线程池创建方法,去创建不同配置的线程池,主要区别在于不同的 ExecutorService 类型或者不同的初始参数。

Executors 目前提供了 5 种不同的线程池创建配置:

  • newCachedThreadPool():可缓存的线程池。当无缓存可用,会创建;闲置时,会回收。线程池大小不做限制,完全依赖操作系统能创建最大线程大小。(内部用SynchronousQueue
  • newFixedThreadPool(int nThreads):使用无界队列,固定长度。超出等等,工作线程退出会创建新工作线程,补足数目。
  • newSingleThreadExecutor():使用无界队列,工作线程数目限制为 1,最多只有一个任务处于活动状态,保证所有任务的顺序执行。
  • newSingleThreadScheduledExecutor()newScheduledThreadPool(int corePoolSize),创建固定长度(1个或多个)线程池,可以进行定时或周期性的工作调度。
  • newWorkStealingPool(int parallelism):内部构建ForkJoinPool,利用Work-Stealing算法并行处理任务,不保证处理顺序。[常被忽略,Java 8 加入]
追问1:几种队列(*3),分别作用(*2)
Queue接口
    |———— BlockingQueue接口(阻塞队列)
        |———— ArrayBlockingQueue类
        |———— DelayQueue类
        |———— LinkedBlockingQueue类
        |———— PriorityBlockingQueue类
        |———— SynchronousQueue类

ArrayBlockingQueue:规定大小的 BlockingQueue , 内部实现是数组。其构造必须指定大小。其所含的对象是 FIFO 顺序排序的。

LinkedBlockingQueue : 大小可选的 BlockingQueue , 若其构造时指定大小,生成就有大小限制;不指定大小,则由 Integer.MAX_VALUE 来决定。其所含的对象是 FIFO 顺序排序的。

PriorityBlockingQueue :类似于 LinkedBlockingQueue , 但是其所含对象的排序不是 FIFO,而是依据对象的自然顺序或者构造函数的 Comparator 决定。

SynchronizedQueue:队列内部仅允许容纳一个元素,对其操作必须是取放交替完成。

追问2:排队策略有哪些?

直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

追问3:线程池的(核心)参数(*4)

corePoolSize : 核心线程数
maximumPoolSize : 最大线程数
keepAliveTime : 如果经过 keepAliveTime 时间后,超过corePoolSize 的线程没接到新任务就回收
unit : 时间单位
workQueue : 用于存储工作工人的队列
threadFactory : 创建线程的工厂
handler : 任务拒绝策略。 当任务队列已满,又有新的任务进来时,会回调此接口。有几种默认实现,通常建议根据具体业务来自行实现

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    
    
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}
追问4:如果提交任务数超过的核心线程数,会发生什么?

提交的任务数超过核心线程数大小后,再提交任务就存放在workQueue。

二、参考

1、第10讲 | 如何保证集合是线程安全的? ConcurrentHashMap如何实现高效地线程安全?
2、第20讲 | 并发包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么区别?
3、如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。
4、Java线程池实现原理及其在美团业务中的实践
5、JAVA线程池参数详解
6、线程池
7、【并发编程】阻塞队列 与 线程池
8、ConcurrentHashMap详解
9、详解线程池的原理及作用

猜你喜欢

转载自blog.csdn.net/HeavenDan/article/details/112907989