前言
在看狂神频道的时候偶然发现下图,感触颇深。特别在当今【程序 = 业务 + 框架】思想盛行的开发者中,夯实基础基础显得格外重要,因此开此专栏总结记录。
对于多线程与并发的知识已经学习过一轮,接下来不断温故而知新哈哈,学习记录回顾:
- 厚积薄发打卡Day25 :狂神说Java之多线程详解<全网最全(代码+笔记)>
- 厚积薄发打卡Day26:狂神说Java之JUC并发编程<代码+笔记>(上)
- 厚积薄发打卡Day27:狂神说Java之JUC并发编程<从JMM到volatile>(中)
- 厚积薄发打卡Day49:狂神说Java之JUC并发编程<CAS入门到“锁“小结>(下)
线程池详解
线程池通过复用线程,避免线程频繁的创建与销毁。
Java的executors 工具类提供了五种类型线程池的创建方法:
我们看看它们的特点和适用场景:
- newFixedThreadPool:
- 固定大小线程池,特点是线程数固定使用的是无界缓冲队列,适用于任务数量不均匀的场景以及对内存压力不敏感,但对系统负载比较敏感的场景
- newCachedThreadPool:
- 缓存线程池,特点是不限制创建的线程数时,适用于要求低延迟的短期任务的场景
- newSingleThreadExecutor:
- 单线程线程池,也就是一个线程的固定线程池,适用于需要异步执行,但需要保证任务执行顺序的场景
- newScheduledThreadPool:
- schedule 的线程池,适用于定期执行任务的场景,支持按固定的频率定期执行和按固定的延时定期执行两种方式
- newWorkStealingPool:
- 工作窃取线程池,使用的ForkJoinPool是固定并行度的多任务队列,适合任务执行时长不均匀的场景
前面提到的线程池,除了工作窃取线程池外,都是通过ThreadPoolExecutor的不同初始化参数来创建的,构造函数的参数列表:
ThreadPoolExecutor:
- corePoolSize 设置核心线程数:
- 默认情况下,核心线程会一直存活
- maximumPoolSize 设置最大线程数决定线程池最多可以创建多少线程
- 第三个参数 keepAliveTime 和第四个参数 uint 用来设置线程的空闲时间和空闲时间的单位
- 当线程闲置超过空闲时间时就会被销毁,可以通过allowCoreThreadTimeOut方法来允许核心线程被回收
- 第五个参数 workQueue 设置缓冲队列:
- 图中左下方的三个队列是设置线程值时最常使用的缓冲队列:
- 其中ArrayBlockingQueue是一个有界队列,就是指队列有最大容量限制
- LinkedBlockingQueue是无界队列,就是队列不限制容量
- 最后一个是SynchronousQueue,是一个同步队列,内部没有缓冲区
- 第六个参数 threadFactory 设置线程池工厂方法:
- 线程工厂用来创建新的线程,可以用来对线程的一些属性进行定制
- 例如线程的group,线程名优先级等,一般使用默认工厂类即可
- 第七个参数设置线程池满时的拒绝策略:
- 如右下角所示有四种策略:
- Abort策略在线程尺码后提交新任务时,抛出rejected execution exception,这个也是默认的拒绝策略
- discard 策略会在提交失败时对任务直接进行丢弃,
- CallerRuns策略会在提交失败时,由提交任务的线程直接执行提交的任务
- DiscardOldest 策略会丢弃最早提交的任务
再来看前面说的几种线程池都是使用怎样的参数来创建的:
-
固定大小线程时:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory); }
- 创建时核心和最大线程数都设置成指定的线程数,这样线程池中就只会使用固定大小的线程数
这种类型的线程池 - 它的缓冲队列使用的是无界队列LinkedBlockingQueue
- 创建时核心和最大线程数都设置成指定的线程数,这样线程池中就只会使用固定大小的线程数
-
single 线程池就是线程数设置为一的固定线程池
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); } public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory)); }
-
cache 的线程池:它的核心线程数,设置为零
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
- 最大线程数是整数integer 的最大值,主要是通过把缓冲队列设置成SynchronousQueue这样只要没有空闲的线程就会新建
-
schedule
- newScheduledThreadPool 线程池与前几种不同的是使用了DelayedWorkQueue这是一种按延迟时间获取任务的优先级队列
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue()); }
我们向线程时提交任务时,可以使用execute 和submit:
- 区别就是submit可以返回一个future 对象,通过future 对象可以了解任务的执行情况,可以取消任务的执行
还可以获取执行结果或者执行异常 - submit 最终也是通过execute 执行的
我们看看图中向线程池提交任务时的执行顺序“
- 向线程池提交任务时,会首先判断线程池中的线程数,是否大于设置的核心线程数
- 如果不大于就创建一个核心线程来执行任务
- 如果大于核心线程数,就会判断缓冲队列是否满了?
- 如果没满,则放入队列,等待线程空闲时来执行
- 如果队列已经满了,就判断是否达到了线程值设置的最大线程数?
- 如果没达到就创建新的线程来执行任务
- 如果已经达到了最大线程数,就会执行指定的拒绝策略
- 这里需要注意队列的判断与最大线程数的判断它们之间的顺序不要搞反
JUC重点工具类实现
JUC 是Java提供的用于多线程处理的工具类库
-
第一行的类都是基本数据类型的原子类,包括:
-
AtomicLong,AtomicInteger,AtomicBoolean是通过unsafe 类实现的
private static final Unsafe unsafe = Unsafe.getUnsafe();
基于CAS (
compareAndSwap
) -
Unsafe是底层工具类,JUC中很多类的底层都使用到了unsafe包装的功能
- unsafe提供了类似c 的指针操作,提供CAS等功能
- unsafe中的所有方法都是native 修饰的
-
另外的LongAdder是JDK1.8 中提供的更高效的操作类:
-
LongAdder 基于cell 实现使用分段所思想是一种以空间换时间的策略,更适合高并发场景
-
Striped64是在java8中添加用来支持累加器的并发组件
-
-
long accumulator 提供了比LongAdder 更强大的功能,能够指定对数据的操作规则
- 例如可以把对数据的相加操作改成相乘操作
-
-
-
第二行中的类提供了对对象的原子读写功能:
-
后两个类AtomicStampedReference,AtomicMarkableReference是用来解决我们前面提到的ABA问题
分别基于:- 时间戳 和 标记位来解决
-
这一页表格中第一行的类主要是锁相关的类
- ReentrantLock
- 与ReentrantLock的独占所不同Semaphore共享锁
- 允许多个线程共享资源,用于限制使用共享资源线程数量的场景
- 例如一百个车辆要使用二十个停车位,那么最多允许二十个车占用停车位
- Stampedlock 是1.8中改进的读写锁是一种使用CLH的乐观锁
- AQS基础——多图详解CLH锁的原理与实现
- 能够有效防止写饥饿:
- 所谓写饥饿就是在多线程读写时读线程访问非常频繁,导致总是有读线程占用资源
写线程很难加上写锁
- 所谓写饥饿就是在多线程读写时读线程访问非常频繁,导致总是有读线程占用资源
- 第二行中主要是异步执行相关的类:
- 这里可以重点了解JDK1.8中提供的,CompletableFuture以支持流式调用,可以方便的进行多future 的组合使用
- 例如可以同时执行两个异步任务,然后对执行结果进行合并处理还可以很方便的设置完成时间
- 另外一个是1.7中提供的ForkJoinPool采用分制思想,将大任务分解成多个小任务来处理,然后再合并处理结果
- ForkJoinPool铺的特点是使用工作窃取算法,可以有效平衡任务执行时间长短不一的场景
- 这里可以重点了解JDK1.8中提供的,CompletableFuture以支持流式调用,可以方便的进行多future 的组合使用
- 这一页表格中第一行是常用的阻塞队列
- LinkedBlockingDeque是双端队列,也就是可以分别从队头和队尾操作入队出队
- ArrayBlockingQueue是单端队列,只能从队尾入队,从队头出队
- 第二行是控制多线程协作时使用的类
- 其中countdown latch 实现计数器功能,可以用来等待多个线程执行任务后进行汇总
- cyclicbarrier 可以让一组线程等待至某个状态后再全部同时执行,一般在测试时使用,可以让多线程更好的并发执行
- 前面已经介绍过用来控制对共享资源的并发访问度
- 最后一行是比较常用的两个集合类:
- concurrenthashmap 前面的课程我们已经详细介绍过了
- 这里可以再了解一下copyOnWriteArraylist,cow通过写入数据时进行copy 修改,然后再更新引用的方式来消除并行读写中的所使用
- 比较适合读多写少,数据量比较小,但是并发非常高的场景
考察点
-
第一个是要理解线程的同步与互斥的原理:
- 包括临界资源,临界区的概念,知道重量级、锁轻量级、锁自选锁、偏向锁、重入锁、读写锁的概念
-
第二点要掌握线程安全的相关机制
- 例如CAS、synchronized和lock 三种同步方式的实现原理
- 要明白ThreadLocal是每个线程独享的局部变量,了解ThreadLocal 使用弱引用的ThreadLocal map 保存不同的threadlocal 变量等等
-
第三点要了解JUC中的工具类:
- 它的使用场景与主要的几种工具类的实现原理:
- 例如ReentrantLock、 concurrenthashmap、LongAdder 等实现方式
-
第四点要熟悉线程池的原理,使用场景常用配置
- 例如:
- 大量短期的任务的场景适合使用cachce的线程池,系统资源比较紧张时可以选择固定线程池,另外
注意慎用无界队列可能会有OOM风险
- 大量短期的任务的场景适合使用cachce的线程池,系统资源比较紧张时可以选择固定线程池,另外
- 例如:
-
第五点要深刻理解线程的:
- 同步与异步
- 阻塞与非阻塞
- 同步与异步的区别是任务是否是在同一个线程中执行的
- 阻塞与非阻塞的区别:
- 异步执行任务时,线程是不是会阻塞等待结果,还是会继续执行后面的逻辑