文章目录
①. ThreadPoolExecutor谈谈你的理解?
- ①. 为什么使用线程池,优势?
- 线程池做的工作主要是控制运行的线程的数量,处理过程中将任务加入队列,然后在线程创建后启动这些任务,如果显示超过了最大数量,超出的数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行
- 它的主要特点为:线程复用 | 控制最大并发数 | 管理线程.
-
②. 线程池如何使用(Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类)
-
③. 方法详解与代码实现
三个方法(掌握)
(1).Executors.newFixedThreadPool(int) : 一池定线程
(2).Executors.newSingleThreadExecutor( ) : 一池一线程
(3).Executors.newCachedThreadPool( ) : 一池N线程
/*
//看cpu的核数
//System.out.println(Runtime.getRuntime().availableProcessors());
* 第四种获取/使用java多线程的方式,线程池
* */
public class ExecutorTest {
public static void main(String[] args) {
//ExecutorService threadPool= Executors.newFixedThreadPool(5);//一池5个处理线程
//ExecutorService threadPool=Executors.newSingleThreadExecutor();//一池一线程
ExecutorService threadPool=Executors.newCachedThreadPool();//一池N线程
try {
for (int i = 1; i <= 10; i++) {
//使用
threadPool.execute(() -> {
//模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程
System.out.println(Thread.currentThread().getName() + "\t 办理业务~!");
});
//try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
}
}catch (Exception e){
}finally {
//关闭
threadPool.shutdown();
}
}
}
public class ExecutorTest {
public static void main(String[] args) {
//ExecutorService threadPool= Executors.newFixedThreadPool(5);//一池5个处理线程
ExecutorService threadPool=Executors.newSingleThreadExecutor();//一池一线程
//ExecutorService threadPool= Executors.newCachedThreadPool();//一池N线程
try {
for (int i = 1; i <= 10; i++) {
//这里也可以传入对应的类,这个类实现了Runnable接口
threadPool.execute(new BatchTackRunnable());
//try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
}
}catch (Exception e){
}finally {
//关闭
threadPool.shutdown();
}
}
}
class BatchTackRunnable implements Runnable{
@Override
public void run() {
//模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程
System.out.println(Thread.currentThread().getName() + "\t 办理业务~!");
}
}
②. 线程池的七大参数
- ①. corePoolSize:线程池中的常驻核心线程数
- 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程
- 当线程池中的线程数目达到corePoolSize后,就会把到达的任务放入到缓存队列当中.
-
②. maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值大于等于1
-
③. keepAliveTime:多余的空闲线程存活时间,当空间时间达到keepAliveTime值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止(非核心线程)
-
④. unit:keepAliveTime的单位
-
⑤. workQueue:任务队列,被提交但尚未被执行的任务(候客区)
-
⑥. threadFactory:表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可(银行网站的logo | 工作人员的制服 | 胸卡等)
-
⑦. handler:拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示 数(maxnumPoolSize)时如何来拒绝
③. 线程池的底层工作原理?
- ①. 还原银行办理业务图
- ②. 解释:
④. 线程池用过吗?生产上你是如何设置合理参数
①. 线程池的拒绝策略请你谈谈
-
①. 等待队列也已经排满了,再也塞不下新的任务了。同时,线程池的maximumPoolSize也到达了,无法接续为新任务服务,这时我们需要拒绝策略机制合理的处理这个问题
-
②. JDK内置的拒绝策略
- AbortPolicy(默认):直接抛出RejectedException异常阻止系统正常运行
- CallerRunsPolicy:"调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是返回给调用者进行处理
- DiscardOldestPolicy:将最早进入队列的任务删除,之后再尝试加入队列
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常.如果允许任务丢失,这是最好的拒绝策略
- ③. 以上内置策略均实现了RejectExecutionHandler接口
②. 你在工作中单一的/固定数的/可变你的三种创建线程池的方法,你用哪个多?超级大坑
-
①. 答案是一个都不用,我们生产上只能使用自定义的
-
②. Executors中JDK给你提供了为什么不用?
参考阿里巴巴java开发手册
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors返回的线程池对象的弊端如下:
(1).FixedThreadPool
和SingleThreadPool
:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
(2).CachedThreadPool
和ScheduledThreadPool
:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
③. 你在工作中是如何创建线程池的,是否自定义过线程池使用
-
①. AbortPolicy: 最大不会抛出异常的值
= maximumPoolSize + new LinkedBlockin gDeque<Runnable>(3) =8
个。如果超过8个,默认的拒绝策略会抛出异常 -
②. CallerRunPolicy: 如果超过8个,不会抛出异常,会返回给调用者去
-
③. DiscardOldestPolicy:如果超过8个,将最早进入队列的任务删除,之后再尝试加入队列
-
④. DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常.如果允许任务丢失,这是最好的拒绝策略
public class MyThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<Runnable>(3),
Executors.defaultThreadFactory(),
//默认抛出异常
//new ThreadPoolExecutor.AbortPolicy()
//回退调用者
//new ThreadPoolExecutor.CallerRunsPolicy()
//处理不来的不处理,丢弃时间最长的
//new ThreadPoolExecutor.DiscardOldestPolicy()
//直接丢弃任务,不予任何处理也不抛出异常
new ThreadPoolExecutor.DiscardPolicy()
);
//模拟10个用户来办理业务 没有用户就是来自外部的请求线程.
try {
for (int i = 1; i <= 10; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
//threadPoolInit();
}
}
④. 合理配置线程池你是如何考虑的?
- ①. CPU密集型
- CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行,通常是配置:CPU + 1
- CPU密集任务只有正在在多核CPU才可能得到加速
- CPU密集型任务配置尽可能少的线程数量
3.1. 避免竞争条件Race Condition:当多个线程同时访问和修改共享的内存数据时,可能会发生竞争条件,导致数据不一致或者程序崩溃。减少线程数量可以减少竞争条件的概率。
3.2. 降低上下文切换开销:线程之间的切换需要保存和恢复线程的上下文信息,这会引入额外的开销。当线程数量过多时,频繁的上下文切换会浪费大量的CPU时间,降低了任务的执行效率。
3.3. 更好的CPU缓存利用率:现代CPU具有多级缓存,较少的线程数量可以更好地利用CPU缓存,减少缓存失效的次数,提高数据访问效率。
- ②. IO密集型
- 由于IO密集型任务线程并不是一直在执行的,配置应尽可能多点,我们通常设置的是CPU * 2
- IO密集型,即该任务需要大量的IO,即大量的阻塞,在单线程上运行IO密集型的任务会导致大量的CPU运行能力在浪费等待
- CPU * 2 或 CPU * 2 + 1:这个建议通常用于估算适合IO密集型任务的线程数。它的背后是考虑到,当一个线程被阻塞等待IO操作完成时,另一个线程可以利用CPU资源执行其他工作,从而充分利用CPU的多核性能。因此,您可以将CPU核心数乘以2或者加上1,作为估计的线程数。这种估算的目的是确保在IO操作等待时能够继续执行其他任务,以提高系统的整体效率。
- CPU核心数 / (1 - 阻塞系数):这个建议考虑了阻塞系数的影响。阻塞系数表示了在某一时间点下,有多少线程处于阻塞状态等待IO完成。这个公式的目的是根据阻塞系数来估算需要的线程数,以充分利用CPU资源。如果阻塞系数为0.5,那么您可以使用CPU核心数 / (1 - 0.5) = CPU核心数 * 2 来估计线程数。这个公式更加精确地考虑了阻塞的影响,因此在一些情况下可能更合适
- 为什么IO密集型比CPU密集型用的线程数量要多一点呢?
对于IO密集型,通常我们设置的线程数量会大于CPU密集型的,这是由于在IO进行网络操作的时候,
系统会切换CPU,让更多的线程去做事情,而不是线程数量不够,一直在等待的状态
Runtime.getRuntime().availableProcessors()
⑤. 不推荐使用快捷的线程池
-
①. 我们需要根据自己的场景、并发情况来评估线程池的几个核心参数,包括核心线程数、最大线程数、线程回收策略、工作队列的类型,以及拒绝策略,确保线程池的工作行为符合需求,一般都需要设置有界的工作队列和可控的线程数
-
②. 任何时候,都应该为自定义线程池指定有意义的名称,以方便排查问题。当出现线程数量暴增、线程死锁、线程占用大量 CPU、线程执行出现异常等问题时,我们往往会抓取线程栈。此时,有意义的线程名称,就可以方便我们定位问题
-
③. newFixedThreadPool方法的源码不难发现,线程池的工作队列直接new了一个LinkedBlockingQueue,而默认构造方法的 LinkedBlockingQueue是一个Integer.MAX_VALUE 长度的队列,可以认为是无界的
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
...
/**
* Creates a {@code LinkedBlockingQueue} with a capacity of
* {@link Integer#MAX_VALUE}.
*/
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
...
}
- ④. 翻看newCachedThreadPool的源码可以看到,这种线程池的最大线程数是Integer.MAX_VALUE,可以认为是没有上限的,而其工作队列 SynchronousQueue是一个没有存储空间的阻塞队列。这意味着,只要有请求到来,就必须找到一条工作线程来处理,如果当前没有空闲的线程就再创建一条新的
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
- ⑤. 线程池的OOM问题,可能是队列满造成的(LinkedBlockingQueue),也可能是线程太多造成的(SynchronousQueue)
⑥. 项目中使用线程池场景
- ①. 后台大数据量导入任务
@Slf4j
public class PoolManager {
private static final int QUEUE_SIZE = 1024;
private static AtomicInteger atomicInteger = new AtomicInteger();
/**
* 商品导入导入线程队列
*/
private static LinkedBlockingQueue<Runnable> importGoodsQueue = new LinkedBlockingQueue<Runnable>(QUEUE_SIZE);
private static ExecutorService importGoodsExecutor = new ThreadPoolExecutor(1, 1, 0L,
TimeUnit.MILLISECONDS, importGoodsQueue, r -> {
Thread thread = new Thread(r);
// 在 Java 语言中线程分为两类:用户线程和守护线程,默认情况下我们创建的线程或线程池都是用户线程,守护线程是为用户线程服务的,当一个程序中的所有用户线程都执行完成之后程序就会结束运行,程序结束运行时不会管守护线程是否正在运行
thread.setDaemon(true);
thread.setName("import-model-thread-" + atomicInteger.getAndIncrement());
return thread;
});
public static void importGoodsBlocking(Runnable runnable) throws Exception {
if (importGoodsQueue.size() >= QUEUE_SIZE) {
throw new Exception("队列已满!");
}
importGoodsExecutor.execute(runnable);
}
}
@Slf4j
public class ImportGoodsTask implements Runnable {
@Override
public void run() {
try{
log.info("ImportGoodsTask == > begin....");
log.info("ImportGoodsTask == > 处理业务逻辑...");
log.info("ImportGoodsTask == > end....");
}catch (Exception e){
log.error("ImportGoodsTask == > 出现了异常:",e);
}
}
}
@Slf4j
@RestController
@RequestMapping("/asyn")
public class AsynController {
@GetMapping("/importGoodsTask")
public void ImportGoodsTask() {
try {
PoolManager.importGoodsBlocking(new ImportGoodsTask());
} catch (Exception e) {
log.error("asyn => ImportGoodsTask 创建商品导入任务异常", e);
}
}
}
- ②. guava刷新数据 - refreshAfterWrites
/**
refreshAfterWrites — 失效后异步刷新缓存
使用refreshAfterWrites后,需要实现CacheLoader的reload方法。需要在方法中创建一个ListenableFutureTask,然后将这个task提交给线程池去异步执行。这样的话,缓存失效后重新加载就变成了异步,加载期间尝试获取取缓存的线程也不会被阻塞。而是获取到加载之前的值。加载完毕之后,各个线程就能取到最新的值。
总结:refreshAfterWrites是异步去刷新缓存的方法,使用过期的旧值快速响应。而expireAfterWrites缓存失效后线程需要同步等待加载结果,可能会造成请求大量堆积的问题。
**/
@SuppressWarnings("all")
public class TestGuavaCache {
private static LinkedBlockingQueue<Runnable> executorQueue = new LinkedBlockingQueue<Runnable>(1024);
private static ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
executorQueue, r -> {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
});
static LoadingCache<String, String> cache = CacheBuilder.
newBuilder().
refreshAfterWrite(2, TimeUnit.SECONDS).
build(new CacheLoader<String, String>() {
//同步加载缓存
@Override
public String load(String key) throws Exception {
System.out.println("============");
return "";
}
//异步加载缓存
@Override
public ListenableFuture<String> reload(String key, String oldValue) throws Exception {
//定义任务。
ListenableFutureTask<String> futureTask = ListenableFutureTask.create(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("================");
return "曹操";
});
//异步执行任务
executorService.execute(futureTask);
return futureTask;
}
});
public static void main(String[] args) throws ExecutionException, InterruptedException {
cache.put("name", "李白");
//第一次获取缓存:李白
System.out.println(cache.get("name"));
//睡眠2s后,再次获取,此时缓存失效,异步的加载缓存。但是线程是立即返回“旧结果”。
Thread.sleep(2000);
System.out.println(cache.get("name"));
Thread.sleep(1000);
System.out.println(cache.get("name"));
}
}
- ③. 小程序楼层组件后台配置商品较多,一次性查询速度慢,使用分批+异步编程思想处理
@Resource(name = "groupGoodsThreadPoolExecutor")
private ThreadPoolExecutor executor;
try {
List<CompletableFuture<List<SearchGoodsResult>>> futures = Lists.partition(spuCodeList, 100)
.stream()
.map(batch -> CompletableFuture.supplyAsync(() -> {
SearchParam searchFutureParam = buildSearchFutureParam(searchParam, batch);
switch (type) {
// 默认、首页楼层商品搜索
case 1:
default:
return elasticService.searchLevelGoods(searchFutureParam);
// 积分商品展示
case 2:
return elasticService.searchIntegralGoods(searchFutureParam);
}
},executor))
.collect(Collectors.toList());
CompletableFuture<Void> allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
// 等待所有搜索任务完成
allOf.join();
List<SearchGoodsResult> searchGoodsResultList = futures.stream()
.filter(future ->CollectionUtils.isNotEmpty(future.join()))
.flatMap(future -> future.join().stream())
.collect(Collectors.toList());
}catch (Exception e) {
logger.error("查询异常:", e);
return null;
}
@Configuration
public class CompletableGoodsThreadConfig {
@Bean(name = "groupGoodsThreadPoolExecutor")
public ThreadPoolExecutor threadPoolExecutor() {
return new ThreadPoolExecutor(8,
8,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(2000),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
}
}