目录
1.线程池的作用
2.线程池的实现
3.自定义创建线程池
1.线程池的作用
当我们使用Thread的实现类来创建线程并调用start运行线程时,这个线程只会使用一次并且执行的任务是固定的,等run方法中的代码执行完之后这个线程就会变成垃圾等待被回收掉。如果是使用实现Runnable接口或者使用实现Callable接口先创建一个任务类,再将任务传递给创建的线程,那这个线程虽然可以用来执行不同的任务,只需要将不同的任务类对象传递给这个线程即可,但仍旧无法解决线程使用的一次性问题。我们希望一个线程不仅是能够通用的,而且还是能够复用的,这就需要线程池来帮忙。
线程池顾名思义就是一个存放线程的池子,当执行一个任务需要线程时就从里面取出一个,用完之后再还回线程池。其特点是:
- 初始状态下线程池是空的,里面没有线程,需要线程时才会创建线程。
- 当线程池创建的线程数还没有达到线程池的容量时,如果有任务需要一个线程但线程池中没有空闲线程,线程池会重新创建一个线程放到线程池中。
- 当线程池创建的线程数达到线程池的容量时,即使线程池中没有空闲线程也不会继续创建,这时那些需要线程的任务会进入一个等待队列,等到其他任务归还线程后才能根据先到先得的原则获取线程。
- 当任务执行完毕后所使用的线程会归还给线程池,此时这个线程又变成空闲线程。
2.线程池的实现
线程池有两种,一种是无限线程池,理论上线程池的容量是无限的,实际上其最大容量为int的最大范围,但由于实际生活中创建一个int范围的线程基本上不能实现,并且也无法同时运行这么多的线程,所以可以说是无限的;另一种则是有限线程池,在创建时需要指定容量。
代码实现:
//实现Runnable接口创建的任务类
public class MyRun implements Runnable{
@Override
public void run() {
for(int i=0;i<100;i++)
System.out.println(Thread.currentThread().getName()+"正在执行MyRun的任务,输出"+i);
}
}
//实现Callable接口创建的任务类
public class MyCall implements Callable<String> {
@Override
public String call() throws Exception {
return Thread.currentThread().getName()+"正在执行MyCall的任务";
}
}
//使用线程池
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
//无限线程池的创建
ExecutorService pool= Executors.newCachedThreadPool();
//有限线程池的创建
ExecutorService pool1=Executors.newFixedThreadPool(3);
//创建两种类型的任务类
Runnable r=new MyRun();
Callable<String> c=new MyCall();
FutureTask<String> ft=new FutureTask<String>(c);
//提交任务,提交之后就会向线程池申请线程并执行任务
pool1.submit(r);
//由于Callable类型的任务的返回值需要FutureTask管理,所以提交的是ft
pool1.submit(ft);
System.out.println(ft.get());
//销毁线程池
pool1.shutdown();
pool.shutdown();
}
}
运行结果:
这里可以发现线程池创建的线程是以“pool-线程池编号-thread-线程编号”来命名的。线程池和所创建的线程的编号都是从1开始,可以查看源码:
在上面的例子中,有限线程池设置的容量为3,但只提交了两个任务,所以不会出现有任务获取不到线程而进入等待队列的情况。
下面测试进入等待队列的情况,提交4个Runnable类型的任务,因为一个任务要循环100次输出耗时较长,可以在提交第四个任务时保证线程池中没有空闲线程,然后让其进入等待队列。利用Debug来查看是否有任务进入了等待队列,先准备好测试代码并加上断点:
开始调试,线程池在创建后可以看到它的属性:
下面执行提交第一个任务:
提交第二个任务:
提交第三个任务:
提交第四个任务:
补充:在实际情况下服务器通常都是一直工作,所以线程池会一直被使用,就没有必要销毁掉,所以一般情况下不会用到销毁操作。
3.自定义创建线程池
一个线程池所创建的线程分为核心线程和临时线程,核心线程则是在创建后会一直存在,直到线程池关闭,而临时线程则是当提交的任务过多时应急使用的,临时线程也会正常工作,但在工作结束后如果在一定时间内没有其他任务,则会被销毁。要注意的是,只有当等待队列的任务占满了整个队列,后面再提交任务时才会创建临时线程,并且创建的临时线程处理的任务并不是等待队列中的任务,而是队列满后后面再提交的任务。如果创建的临时线程达到最大数量,此时仍有任务被提交时就需要选择一种应对策略来处理后面提交的任务。
自定义线程池需要设置以下七种参数:
- 允许创建的最大核心线程数量(不能小于0)
- 允许创建的最大线程数量(不能小于0且必须>=核心线程数量,最大线程数量减去最大核心线程数量就是可以创建的最大临时线程数量)
- 临时线程的最大空闲时间1(设置时间的值,不能小于0)
- 临时线程的最大空闲时间2(设置时间的单位,使用TimeUnit指定)
- 等待队列(其实就是一个阻塞队列,不能为null)
- 创建线程的工厂(也就是怎样创建一个线程,不能为null)
- 应对策略(一共有四种策略,四选一,一般选择第一个,不能为null)
自定义创建线程池代码实现:
//创建自定义线程池
ThreadPoolExecutor pool2=new ThreadPoolExecutor(
3, //设置核心线程的最大创建数量
5, //设置最大线程数量(最大临时线程数量也就是5-3=2)
60, //设置临时线程的最大空闲时间的值部分
TimeUnit.SECONDS, //设置临时线程的最大空闲时间的单位部分
new ArrayBlockingQueue<>(5), //加入阻塞队列作为等待队列
Executors.defaultThreadFactory(), //使用java提供的线程池默认创建线程的工厂作为创建线程的工厂即可
new ThreadPoolExecutor.AbortPolicy() //选择第一种应对策略,AbortPolicy是一个内部类
);
使用方式和普通线程一样,提交任务用submit,销毁用shutdown。
补充内部类:内部类是在一个类中定义另一个类。当这个类需要依附于另一个类但这个类本身也是独立的一部分时可以将这个类创建为其他类的内部类,就比如发动机和汽车,发动机需要依附于汽车才能发挥作用,但发动机本身又是独立的一部分。
那线程池的最大线程数量是不是越大越好呢?其实不然,线程池的最大线程数量通常是按照规定来的,这取决于开发的项目是CPU密集型还是I/O密集型的。
CPU密集型也就是所开发的项目需要进行的运算偏多,而执行运算操作就要用到CPU。这种项目所需的最大线程数量应当设置为:最大并行数+1。
所谓最大并行数就是看CPU能最多能分给java多少线程,通常说CPU是多少核多少线程的,核数就是这个CPU有多少大脑,多少线程就是这个CPU有多少只手,每只手对应一个线程,但不一定所有的线程都可以让java调度,我们可以通过下面的代码查看可以分给java的最大线程数:
//查看CPU能分给java的最大线程数
int num=Runtime.getRuntime().availableProcessors();
System.out.println(num);
可以分配给java的最大线程数就是最大并行数,至于要加1是为了当已经创建了的某个线程出现问题时可以利用这个多出来的线程继续工作, 尽可能地将CPU利用率最大化。
I/O密集型就是开发的项目中I/O操作比较多,现在大多数项目都是I/O密集型的。这种情况下线程池的最大线程数量可以设置为:最大并行数*期望CPU的利用率*(总的运行时间/CPU的运行时间)。通常情况下我们希望CPU的利用率越高越好,所以可以设置为100%,最大并行数在CPU密集型部分介绍过了,那什么是CPU的运行时间呢?
比如要执行读取文件中的两个整数并相加的操作,这个操作分为两部分,从文件中读取数据的部分没有用到CPU,而后面的相加部分属于运算,就需要CPU了。所以在这个例子中,CPU的运行时间就是后面相加所需的时间。在实际项目中可以使用thread dump工具来测总的运行时间和CPU的运行时间。