目录
2.单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行
4.提供了"延迟"和"周期执行"功能的ThreadPoolExecutor
线程池
自定义线程池核心参数
//1:获取线程池的对象
ThreadPoolExecutor pool = new ThreadPoolExecutor(
3,//核心线程数量,不能小于0
6,//最大线程数,不能小于0(最大线程数 >= 核心线程数)
60,//临时线程最大存活时间,不能小于0
TimeUnit.SECONDS,//临时线程最大存活时间单位
new ArrayBlockingQueue<>(3),//阻塞队列
Executors.defaultThreadFactory(),//创建线程工厂
new ThreadPoolExecutor.AbortPolicy()//任务的拒绝策略
);
//2:提交任务
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
pool.submit(new MyRunnable());
以上代码解释:
前三个任务会分三个核心线程去完成,
后两个任务先放到阻塞队列中等待。
那三个核心线程执行完自己的任务空闲下来时,就会去按顺序完成阻塞队列中的任务
临时线程:临时线程空闲时间超过最大空闲时间就会被释放。
核心线程不会被释放。
线程池执行原理
核心原理
1.创建一个池子,池子中是空的。
2.提交任务时,池子会创建新的线程对象,任务执行完毕,线程归还给池子。
下回再次提交任务时,不需要创建新的线程,直接复用已有的线程即可。
3.但是如果提交任务时,池子中没有空闲线程(空闲下来的核心线程),也无法创建新的线程(核心线程数量已经到达最大值了),任务就会排队等待
利用JAVA工具类获得线程池对象
这种利用工具类创建线程池对象的方法不止于此,后面介绍线程池的种类时还有更为深入的说明。
不推荐用这种方法创建,最好还是用自定义线程池的方法(可以根据硬件条件、不同的业务需求等设置不同的参数)。
三个临界点
不断的提交任务,会有以下三个临界点:
1.当核心线程满时,再提交任务就会排队
2.当核心线程满,队伍满时,会创建临时线程
3.当核心线程满,队伍满,临时线程满时,会触发任务拒绝策略
任务拒绝策略
线程池中常见的阻塞队列
workQueue-当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建临时线程执行任务:
1.ArrayBlockingQueue:基于数组结构的有界阻塞队列,FIFO
2.LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO
3.DelayedWorkQueve:是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的任务
4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作
有界指可以设置阻塞队列容量。
默认无界:LinkedBlockingQueue队列默认容量为Integer的最大值。
核心线程数设置为多大
最大并行数(CPU核数)
例
说明这台电脑的CPU是4核8线程的,最大并行数(CPU核数)就是8。
最大并行数计算方法:
计算方法
上下文切换
多线程编程中一般线程的个数都大于CPU核心的个数,而一个CPU核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效的执行,CPU采取的策略是为了每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让其他线程使用,这个过程就属于一次上下文切换。
当线程池中核心线程数量过大时,线程与线程之间会争取CPU资源,这样就会导致上下文切换。过多的上下文切换会增加线程的执行时间,影响了整体执行的效率;
CPU密集型任务
例如:计算型代码,Bitmap转换,Gson转换等计算型任务是CPU密集型任务
这种任务消耗的主要是CPU资源。线程在执行任务时会一直利用CPU,CPU使用率很高,而I/O执行很快。那么这时候就需要尽量减少线程上下文切换这种额外的时间开销。
所以核心线程数设置为CPU核数+1就好。
比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
IO密集型任务
例如:文件读写,DB读写,网络请求等任务是IO密集型任务
这种任务用大部分的时间来处理 IO 交互,即大部分时间都阻塞在IO上,而线程在处理 IO 的时间段内不会占用 CPU 来处理(CPU只需要对相应的文件进行通知一声,不需要进行过多的运算),这时就可以将CPU交出给其它线程使用。因此在 IO 密集型任务的应用中,我们可以多配置一些线程。
一般来说核心线程数设置为CPU核数*2+1
混合型任务
混合型任务:既包含CPU密集型又包含I/O密集型。
核心线程数 = CPU核数 * (总时间(CPU计算时间+CPU等待时间)/ CPU计算时间)
CPU计算时间和CPU等待时间可以通过thread dump工具来计算。
总结
CPU密集型 可以理解为 就是处理繁杂算法的操作,对硬盘等操作不是很频繁,比如一个算法非常之复杂,可能要处理半天,而最终插入到数据库的时间很快。
IO密集型可以理解为 简单的业务逻辑处理,比如计算1+1=2,但是要处理的数据很多,每一条都要去插入数据库,对数据库频繁操作。
JAVA开发的项目大部分是IO密集型的。
线程池的种类
介绍
在java.util.concurrent.Executors类中提供了大量创建线程池的静态方法,常见的就有4种。
1.创建使用固定线程数的线程池
核心线程数与最大线程数一样,没有临时线程。
适用于任务量已知,相对耗时的任务。
2.单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行
核心线程数和最大线程数都是1。
适用于按照顺序执行的任务。
3.可缓存线程池
核心线程数为0,最大线程数是integer.max.value
阻塞队列为synchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作。
适合任务数比较密集,但每个任务执行时间较短的情况。
4.提供了"延迟"和"周期执行"功能的ThreadPoolExecutor
延迟:可以延迟任务的执行,即可以设置任务的开始执行时间为一定时间之后。
弊端
OOM:内存溢出
所以不建议用Executors创建线程池。