是从什么时候开始写博客的,如果是正式的开始,那么大概也就是4个月吧,但是如果说是有这个想法,那么很久之前就有了,学习要有输入和输出才能更好的吸收,就像很多人说的,你得用,用了才知道好坏,才能更好的掌握,可是,很多东西我们无法用到项目上,比如,我们工作中,项目的体谅很小,并发量小,数据量小,访问量小,如果用上我们的学到的多线程技术,那就是杀鸡用牛刀,过度的设计,反而拖慢了系统的运行,在这种情况下谈何使用,然而,技术不仅仅可以输出在项目上,也可以输出在博客上,代码的运行方式有很多种,不应该因为项目用不到,就停止输出。
下面来说说线程池
首先,我们先new一个线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 10,
0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(10));
threadPoolExecutor.execute(() -> {
System.out.println("Hi 线程池!");
});
threadPoolExecutor.shutdown();
这个是最基本的线程池创建,如果能够熟悉传入的参数的话,那也还挺简单的。
下面简单说说,这几个参数,和运行时发生的事
corePoolSize 线程基本大小10 但是如果多了会超出 maximumPoolSize 10 线程最大线程
如何运行的线程池满了 就往队列放
队列如果满了 并且当前线程小于最大线程
就会把线程放到线程池 此时就会超出线程的基本大小
keepAliveTime 线程执行完任务后存活的时间
TimeUnit.MILLISECONDS 是时间的单位 ,使用这个队列 new ArrayBlockingQueue<>(10)
这个就是线程池的基本运行,那么为什么用到线程池呢,简单点说,就是让线程重复的运行,减少cpu的开销,因为cpu负责分配线程。
当然我们也有更牛逼的说法,就是符合科特尔法则和阿姆达尔定律。为什么这么说,先解释下科特尔法则是啥,
利特尔法则的内容 Lead Time = 存货数量×生产节拍
任一项目从完工日期算起倒推到开始日期这段生产周期,称为提前期。
举个栗子
我们买票上车需要排队,然后如果前面有20个人,每个人要用2分钟,那么就需要40分钟,那这 段时间在科特尔就叫做提前期,而多线程就是能改变这个生产节拍。
然后说下阿姆达尔定律
阿姆达尔曾致力于并行处理系统的研究。对于固定负载情况下描述并行处理效果的加速比s,阿姆达尔经过深入研究给出了如下公式:
S=1/(1-a+a/n)
其中,a为并行计算部分所占比例,n为并行处理结点个数。
看起来挺晦涩的,那他用在哪呢,我们看到线程池的入参有,线程池中线程的数量,以及最大线程数,还有队列的长度,那这个长度改怎么定义呢,这时候我们就用他了,
如果我们要达到某个数量的 QPS(每秒访问量),我们使用如下的计算公式。
设置的线程数 = 目标 QPS/(1/任务实际处理时间)
举例说明,假设目标 QPS=100,任务实际处理时间 0.2s,100 * 0.2 = 20个线程,这里的20个线程必须对应物理的20个 CPU 核心,否则将不能达到预估的 QPS 指标。
但实际上我们的线上服务除了做内存计算,更多的是访问数据库、缓存和外部服务,大部分的时间都是在等待 IO 任务。
如果 IO 任务较多,我们使用阿姆达尔定律来计算。
设置的线程数= CPU 核数 * (1 + io/computing)
看看吧,有时候简单还真不是最好的,就像产品写的需求一样。
下面介绍下线程池的种类
这个种类应该说是工具类创造出来的东西,真正的线程池千变万化
线程池包中四种实现 Executors工具类中提供的方法
/**
* 线程池包中四种实现线程之一 Executors 工具类中提供的方法
* 创建一个单线程的线程池,如果出现意外终止线程,再创建一个线程
* 和Fixed那个线程池一样,他也是用的无界队列,也会可能出现OOM
*/
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 1; i < 5; i++) {
int groupId = i;
executorService.execute(() -> {
for (int j = 1; j < 5; j++) {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
logger.info("第 {} 组任务,第 {} 次执行完成", groupId, j);
}
});
}
executorService.shutdown();
}
/**
* 线程池包中四种实现线程之一 Executors 工具类中提供的方法
* 它可以延迟定时执行,有点像我们的定时任务。同样它也是一个无限大小的线程池 Integer.MAX_VALUE。它提供的调用方法比较多,
* 包括:scheduleAtFixedRate、scheduleWithFixedDelay,可以按需选择延迟执行方式。
* 他也是一个无限的队列 也有OOM的风险
*
*/
public static void main(String[] args) {
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);
executorService.schedule(() -> {
logger.info("3秒后开始执行");
}, 3, TimeUnit.SECONDS);
executorService.scheduleAtFixedRate(() -> {
logger.info("3秒后开始执行,以后每2秒执行一次");
}, 3, 2, TimeUnit.SECONDS);
executorService.scheduleWithFixedDelay(() -> {
logger.info("3秒后开始执行,后续延迟2秒");
}, 3, 2, TimeUnit.SECONDS);
}
/**
* 线程池包中四种实现线程之一 Executors 工具类中提供的方法
* fixed 固定
* 官方介绍 创建一个固定大小可重复使用的线程池,以 LinkedBlockingQueue 无界阻塞队列存放等待线程
* 调用的其实是这个
* new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
* 因为使用了无界队列,所以他就没有最大的 线程池数量,而且队列也就不用设置空间
* 存在一个问题是,队列是无界的如果一直放入,会导致OOM
*/
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 1; i < 5; i++) {
int groupId = i;
executorService.execute(() -> {
for (int j = 1; j < 5; j++) {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
logger.info("第 {} 组任务,第 {} 次执行完成", groupId, j);
}
});
}
executorService.shutdown();
}
/**
* 线程池包中四种实现线程之一 Executors 工具类中提供的方法
* 首先 SynchronousQueue 是一个生产消费模式的阻塞任务队列,只要有任务就需要有线程执行,线程池中的线程可以重复使用。
* 比较前两种,这种也就是队列不同,这种也可能发生OOM
*/
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 1; i < 5; i++) {
int groupId = i;
executorService.execute(() -> {
for (int j = 1; j < 5; j++) {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
logger.info("第 {} 组任务,第 {} 次执行完成", groupId, j);
}
});
}
executorService.shutdown();
}
这四种线程池就是4中api,其实更多时候还是我们自己手动去创建更好一些,因为我们发现4中线程池使用的都是无界队列,存在oom的风险。