一.前情提要
1.线程存在的意义:在并发编程中,由于使用进程来编程"太重了".此时引入了线程("轻量级进程").
2.线程优势:创建线程比创建进程更高效,销毁线程比销毁进程更高效,调度线程比调度进程更高效.
俗话说没有对比就没有伤害,线程一切的优势都是对比进程而来的,但随着我们对于性能要求的提高,我们发现创建线程好像也并没有那么的"轻量",当我们需要频繁的创建和销毁线程的时候,所需的资源开销也不小.那么有没有让效率更加高的办法呢?
二.改善线程效率的方法
既然线程是一种"轻量级进程",那么我们能不能再发明一种轻量级的线程呢?这种想法当然可行的,协程/纤程就是如此创造出来的,可惜遗憾的是Java的标准库中并不支持,所有这种方法在Java里只能作罢.(Go语言爆火的原因之一就是内置了协程)
那么没有其他方法了嘛,这时聪明的Java大佬们想到了用线程池来改善线程的效率问题.通过线程池来降低线程创建和销毁的开销.
三.线程池的基本原理
线程池原理与字符串常量池,数据库连接池类似,我们事先把需要创建的线程创建好放进线程池中,在我们后面需要这个线程时直接从池中获取,用完后再归还给线程池即可.这两个动作比创建和销毁就要高效一些了.
高效的原因其实也很简单,因为创建线程和销毁线程都需要交给操作系统的内核来完成(不可控),但是从线程池中取线程和还线程是由用户自己或者是程序员来控制的(可控).这就好比你去银行办理业务,你去找柜台的工作人员办理就是交给操作系统内核完成,这个过程中银行的工作人员不一定会马上帮你办理业务,可能他要先上个厕所,在喝个水,或者先帮别人办理(不可控).但是我们要是自己去ATM机上办理就保证了自己的办理时间(可控),这让我们办理业务的时间总是小于等于去找工作人员办理的时间.
四.Java标准库中的线程池
1.标准库中创建线程池
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadDemo {
public static void main(String[] args) {
//创建一个有10个线程数目的线程池
ExecutorService pool = Executors.newFixedThreadPool(10);
}
}
线程池中提供了一个重要方法, submit,可以给线程池提交若干个任务.
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("线程池");
}
});
此时的run方法就并不是由主线程来调用了,而是线程池中10个线程中的某一个线程来进行调用.从打印的结果我们也发现虽然run方法中的任务执行完毕,但是并没有提示进程结束,这是由于线程池里的线程都是前台线程,它们会阻止进程结束.
2.了解标准库中的线程池
线程数量动态变化,任务多则线程池里的线程多,任务少则线程池里的线程少.
线程池里只有一个线程.
类似于定时器,让任务延时执行,不过执行的时候不是由扫描线程自己执行,而是由单独的线程来执行.
上述所有线程池本质都是通过包装ThreadPoolExecutor来实现出来的.
3.了解ThreadPoolExecutor
让我们打开Java的官方文档找到ThreadPoolExecutor吧.
五.深入了解ThreadPoolExecutor的构造方法
由官方文档中的构造方法我们可以将其拆分为下列七种.
1.corePoolSize(核心线程数)
2.maximumPoolSize(最大线程数)
核心线程数好比公司里的正式员工,要为公司所面对的问题进行兜底,而最大线程数就是公司里的所有员工(正式员工+实习生),在公司人手不够的时候多招些实习生干活,活少些的时候可以进行开除(即线程销毁).
那么实际开发中,线程池中的线程线程数设置为多少合适呢?不同的程序特点不同,此时需要设置的线程数量也各不相同.先考虑两个极端情况:
1)CPU密集型:每个线程执行的任务都需要cpu来进行运算.
在cpu密集型的情况下,我们设置的线程数量应该小于等于cpu的核心数量,因为设置再多的线程数量只会增大开销,cpu根本运转不过来.
2)IO密集型:每个线程干的活都是等待IO(如读写硬盘,读写网卡,等待用户输入......).
在IO密集型的情况下,线程调度已经不受制于cpu的核数了,理论上我们可以设置的线程数量为无穷大都可以,当然实际上不行的,但线程数设置个几百个还是没什么问题的.
回归现实,在实际开发中我们几乎不可能遇上这两种理想模型,真实的程序往往需要一部分吃cpu,一部分吃io,具体要怎么划分那就只能说实践才是检验真理的唯一标准了.只有通过测试/实验的方式才可以更准确的知道设置多少个线程数量更好.
所以我们应该在自己的机器环境下,综合考虑执行结果,执行时间,cpu占用率,内存占用情况等因素来最终决定线程的设置数.
3.long keepAliveTime(描述公司实习生可以摸鱼的最大时间)
4.TimeUnit unit(时间单位,如s,ms,min...)
5.BlockingQueue<Runnable> workQueue(线程池的任务队列)
6.ThreadFactory threadFactory(用于创建线程)
7.RejectedExecutionHandler handler(线程池的拒绝策略,即当线程任务满时再添加任务的处理)
六.线程池的拒绝策略
查看Java官方文档可以看到,线程池给我们一共提供了四种拒绝策略,如下:
1.任务太多导致队列满了则抛出异常.
2.任务太多队列满了,那么多出来的任务谁加的谁执行.
3.任务太多队列满了,则丢弃最老的任务.
4.任务太多队列满了,则丢弃最新的任务.
举例来说吧,当老师布置的作业太多达到小寰极限的时候,如果老师再继续布置作业,那么小寰有如下四种处理方式:
1.直接抱头痛哭,什么作业也不写了.
2.谁再布置作业谁自己来写.
3.不写最开始布置的作业,写新布置的作业.
4.不写最新的作业.
七.自己实现一个简单的线程池
直接上代码吧.
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.BlockingDeque;
class MyThreadPool {
//定义阻塞队列存放任务
private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
//n表示线程数量
public MyThreadPool(int n) {
//创建n个线程
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
while(true) {
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
//注册任务给线程池
public void submit(Runnable runnable) {
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}