【面试】Java并发篇(二)

并发工具类:比较成熟的解决方案有哪些?这些方案底层原理是什么?优缺点?应用?

0、概要

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

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

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

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

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

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

并发全景图

一、互斥

1.1 无锁

1、不变模式
2、线程本地存储
3、CAS
4Copy-on-Write

1、CAS是什么?

答:CAS 操作中包含三个操作数: 
  - 需要读写的内存位置V 
  - 进行比较的预期原值A 
  - 拟写入的新值B

  if isMatched(V, A)  then A=B 
  else then A不变 
追问1:CAS会导致什么问题?如何解决?
问题方案
ABA引入版本号
开销大退出机制:设置重试次数/阈值
单共享变量把多个共享变量合并成一个,可以把多个变量放到一个对象[AtomicReference]
追问2:版本号如何实现? 【补充】

答:

  1. 获取当前version

  2. if version = oldVersion, then set version = newVersion;
    else update failed

  3. 核心SQL

update table set name = 'Aron', version = version + 1
where id = #{id} and version = #{version};
追问3:CAS,你有哪些应用?

……

1.2 互斥锁

1Synchronized
2Lock
3、读写锁

0、悲观锁和乐观锁(*2)区别和应用场景;

类型乐观锁悲观锁
概念先用,实际更新时再判断。如果没更新数据,表示成功,否则回滚重试。先获取锁,再进行业务操作。
实现版本号、CASJava-synchronized(1.6后有偏向锁+轻量级锁优化); MySQL-读锁、写锁、行锁
场景读多写少读少写多

1、Synchronized

【面试】Synchronized常见面试题

2、ReentrantLock底层实现原理(*2)

4
ReentrantLock通过重写锁获取方式和锁释放方式这两个方法实现了公平锁和非公平锁。

首先ReentrantLock继承自父类Lock,然后有3个内部类,其中Sync内部类继承自AQS,另外的两个内部类继承自Sync,这两个类分别是用来公平锁和非公平锁的。

通过Sync重写的方法tryAcquire、tryRelease可以知道,ReentrantLock实现的是AQS的独占模式,也就是独占锁,这个锁是悲观锁。
……

3、Java读写锁,

与传统锁不同的是读写锁的规则是可以共享读,但只能一个写,总结起来为:读读不互斥,读写互斥,写写互斥,而一般的独占锁是:读读互斥,读写互斥,写写互斥,而场景中往往读远远大于写,读写锁就是为了这种优化而创建出来的一种机制。注意是读远远大于写,一般情况下独占锁的效率低来源于高并发下对临界区的激烈竞争导致线程上下文切换。因此当并发不是很高的情况下,读写锁由于需要额外维护读锁的状态,可能还不如独占锁的效率高。因此需要根据实际情况选择使用。

追问1:为什么并发读要加锁?

读锁是防止读到写的中间值。

二、协作

三、分工

1、线程池
2Fork/Join
3Future
4Guarded Suspension模式
5Balking模式
6、thread-Per-Message模式
7、生产者-消费者模式
8Worker-Thread模式
9、两阶段终止模式

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

四、并发容器

4.2 线程安全容器:ConcurrentHashMap、ConcurrentSkipListMap、CopyOnWriteArrayList

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

#### 追问1HashMap/HashTable/ConcurrentHashMap结构,底层(*4),如何保证线程安全,怎么实现(*3)
#### 追问2ConcurrentHashMap1.71.8的区别(*2

【面试】ConcurrentHashMap常见面试题(待完善)

4.3 线程安全队列(Queue/Deque):ArrayBlockingQueue、SynchronousQueue、PriorityBlockingQueue

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

五、其他

1、Volatile(*2)

1、【面试】Volatile常见面试题

2、Volatile和Synchronized的区别

关键字volatilesynchronized
使用级别仅能修饰变量可修饰变量、方法、和类
要素保证仅保证可见性、有序性可保证可见性、有序性、原子性
阻塞不会造成线程阻塞可能会
编译器优化标记变量不会被优化标记变量可以被优化

本质对比:

  • volatile本质是告诉JVM当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
  • synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

四、参考

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值