目录
为何使用线程池而不直接创建线程:
线程池内有一些线程,用到线程执行任务时不再需要从系统申请新的线程,而是直接从池子中拿取使用,当使用完毕后还要还给池子。从线程池内拿取线程是用户态内的操作,而创建新的线程涉及到内核态操作,而内核只有一个,还需要支持其他程序运行,所以内核时非常忙的,不一定何时才能创建线程,所以创建线程的时间时不可控的。Java标准库提供了现成的线程池
简述线程池的工作步骤:
通过创建线程池,线程池内有内置的线程,其他线程通过submit方法向线程池内提交任务,把这些任务添加到一个阻塞队列中,然后线程池内的所有线程一直从阻塞队列中提取任务,并调用run方法,执行每一个任务。
线程池的创建:
构造方法参数:
corePoolSize:核心线程数
maximumPoolSize:最大线程数
KeepAliveTime:非核心线程的存活时间
unit:非核心线程的存活时间的单位
(任务较多的时候会创建一些非核心线程,任务少的时候过了非核心线程的存活时间后,非核心线程就会销毁)
workQueue:用来管理任务的阻塞队列,程序猿可以手动指定一个阻塞队列
threadFactory:线程工厂,用于创建线程的辅助的类
RejectedExecutionHandler:拒绝策略,即线程池满的时候采取拒绝新线程的拒绝策略
几种拒绝策略:
AbortPolicy:如果线程池内的任务满了,继续向线程池内添加任务直接报错
CallerRunsPolicy:如果线程池任务满了,继续向线程池内添加任务,让添加任务的线程自己执行该任务
DiscardOldestPolicy:抛弃最老的任务(即阻塞队列队首元素),让新添加的任务入队
DiscardPolicy:抛弃最新的任务(即阻塞队列队尾元素),让新添加的任务入队
通过工厂模式创建线程池:
在绝大多数情况下,我们一般都是使用工厂模式创建线程池,而不是使用构造方法,以下为常用的几种线程池:
//创建总线程数与核心线程数均固定为10的线程池,即意味着没有非核心线程,同一时间内最多有10个线程在活跃
//当任务总数超过10的时候,会出现有任务在队列中等待空闲线程的出现,当有线程异常时,会自动补足线程,保持总线程数一直为10
ExecutorService pool1 = Executors.newFixedThreadPool(10);
//创建一个核心线程数为0的线程池,使用的都是非核心线程,如果非核心线程60秒内一直是空闲状态,就会销毁该线程
//这样的线程池适合做一些短时间任务
ExecutorService pool2 = Executors.newCachedThreadPool();
//只有一个核心线程的线程池,没有非核心线程
ExecutorService pool3 = Executors.newSingleThreadExecutor();
//含有10个核心线程的定时器线程池,有schedule方法,类似与Timer的使用方法
//区别在于Timer内只有一个线程,极大概率因为前置任务执行时间过长导致后续任务延期执行
ScheduledExecutorService pool4 = Executors.newScheduledThreadPool(10);
//含有一个核心线程的定时器线程,与Timer极其类似,都会因为前置任务执行时间过长导致后续任务延期
//但是Timer内的线程如果挂了就全毁了,而该线程池内如果线程挂了,会有新的线程被创建用来顶替挂掉的线程
ScheduledExecutorService pool5 = Executors.newSingleThreadScheduledExecutor();
为何采用工厂模式:
以实例化一个点(Point)对象为例子:我们都知道在二维平面中一个点可以通过xy坐标表示,也可以通过极坐标表示,然而这两种构造方法的参数表都是同样的类型,个数也都是一样的,不能构成方法重载,所以我们采取定义一个BuildPoint类,内置两种名字不同的方法来构造点对象:
线程池执行效果:
public static void main(String[] args) {
//固定3线程的线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
for (int i = 1; i < 10; i++) {
int number = i;
threadPool.submit(() -> {
System.out.print("hello" + number + " ");
});
}
}
这里创建的线程池是固定3线程的线程池,main方法后续向该线程submit了9个任务,线程池内的三个线程合伙执行完这9个任务,因为线程调度的无序性,所以执行结果是无序的,第二次重新执行的结果极大概率也是不同的。
我们看到完成这9个任务后程序没有结束,这是因为线程池内3个线程是前台线程,且因为向队列中取出任务,但因为任务已经没有了,进入了阻塞状态
有一点需要注意的是,main方法内需要创建一个新的变量number来记录i才能打印i的值,因为变量捕获机制中捕获的变量必须是被final修饰的或者是实际final(没有被修改过)的,而i在一直修改,所以每次循环都需要number记录i,因为number没有变化过,所以number是可以被捕获的
简单模拟一个线程池:
class MyThreadPool{
//用阻塞队列存储任务
//让线程池内创建的线程取出阻塞队列中的任务,并在线程内调用run方法,从而可以实现有限的线程执行无限个添加的任务
private BlockingQueue<Runnable> queue = new LinkedBlockingDeque<>();
public void submit(Runnable runnable) throws InterruptedException {
//向阻塞队列中添加任务
queue.put(runnable);
}
public MyThreadPool(int n) {
//这里的n代表创建的线程的个数
for (int i = 0; i < n; i++) {
//创建n个线程,每个线程内从阻塞队列中取出任务,并调用任务的run方法
//执行完一个任务后因为有while(true),就继续从阻塞队列中取出任务
//这样的线程一共n个,相当于n个人的团队将一个大问题分解成小问题,每个人都解决小问题
//一直做的没有问题可做
Thread thread = new Thread(() -> {
while (true){
try {
Runnable runnable = queue.take();
//没有为每个任务单独创建线程
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
}
}
线程池创建多少个线程合适:
结论:
根据自己的项目一遍遍测试,根据运行时间与资源占用情况等选出效果最好的一种做法
原因:
不同的程序内线程做的任务不一样
有的线程做的任务是cpu密集型任务,主要做一些计算工作,需要在cpu上运行
有的线程做的是IO密集型任务,主要是等待IO操作(等待读写硬盘,读写网卡等)这种任务不怎么占用cpu
极端情况下如果所有的任务都是cpu密集型任务,那么线程数就不应该超过cpu的逻辑核心数,如果所有任务都是IO密集型任务,那么线程数应该远超过cpu核心数
但实际情况下,各种任务都有,所以需要手动测试
(这里的逻辑核心数指的是cpu内的逻辑处理器个数)