1.获取多线程的方法
- 继承Thread类,重写Thread类的run()
- 实现Runnable接口
- 实现Callable接口
- 使用线程池获取
2.说一下Callable接口
重点说一下Callable接口,是一种让线程执行完成后,能够返回结果的。
/**
* Callable有返回值
* 批量处理的时候,需要带返回值的接口(例如支付失败的时候,需要返回错误状态)
*
*/
class MyThread2 implements Callable<Integer> {
@Override
public Integer call() throws Exception {
System.out.println("come in Callable");
return 1024;
}
}
这里需要用到的是FutureTask类,并且还需要传递一个实现Callable接口的类作为构造函数。
//FutureTask:实现了Runnable接口,构造函数又需要传入 Callable接口
// 这里通过了FutureTask接触了Callable接口
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
//然后在用Thread进行实例化,传入实现Runnabnle接口的FutureTask的类
Thread t1 = new Thread(futureTask, "aaa");
t1.start();
//最后通过 futureTask.get() 获取到返回值
// 输出FutureTask的返回值
System.out.println("result FutureTask " + futureTask.get());
//也就是说 futureTask.get() 需要放在最后执行,这样不会导致主线程阻塞
重点!重点!重点!
注意
多个线程执行 一个FutureTask的时候,只会计算一次
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
// 开启两个线程计算futureTask
new Thread(futureTask, "AAA").start();
new Thread(futureTask, "BBB").start();
如果我们要两个线程同时计算任务的话,那么需要这样写,需要定义两个futureTask
FutureTask<Integer> futureTask = new FutureTask<>(new MyThread2());
FutureTask<Integer> futureTask2 = new FutureTask<>(new MyThread2());
// 开启两个线程计算futureTask
new Thread(futureTask, "AAA").start();
new Thread(futureTask2, "BBB").start();
3.线程池----ThreadPoolExecutor
为什么用线程池?(就是说一下使用线程池的好处呗?)
- 多核处理的好处是,省略的上下文的切换开销
- 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就立即执行
- 提高线程的可管理性。线程是稀缺资源,如果无线创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
创建线程池方法?
- Executors.newFixedThreadPool(int i) :创建一个拥有 i 个线程的线程池执行,长期的任务,性能好很多,创建一个定长线程池,可控制线程数最大并发数,超出的线程会在队列中等待
- Executors.newSingleThreadExecutor:创建一个只有1个线程的 单线程池,一个任务一个任务执行的场景,创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行
- Executors.newCacheThreadPool(); 创建一个可扩容的线程池,执行很多短期异步的小程序或者负载教轻的服务器,创建一个可缓存线程池,如果线程长度超过处理需要,可灵活回收空闲线程,如无可回收,则新建新线程
- Executors.newScheduledThreadPool(int corePoolSize):线程池支持定时以及周期性执行任务,创建一个corePoolSize为传入参数,最大线程数为整形的最大数的线程池
4.线程池execute与submit的区别
- execute只能提交Runnable类型的任务,没有返回值,而submit既能提交Runnable类型任务也能提交Callable类型任务,返回Future类型。
- execute方法提交的任务异常是直接抛出的,而submit方法是是捕获了异常的,当调用FutureTask的get方法时,才会抛出异常。
线程池的重要参数
线程池在创建的时候,一共有7大参数
- corePoolSize:核心线程数,线程池中的常驻核心线程数
- maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
- keepAliveTime:多余的空闲线程存活时间
- unit:keepAliveTime的单位 workQueue:任务队列,被提交的但未被执行的任务(类似于银行里面的候客区)
- LinkedBlockingQueue:链表阻塞队列 SynchronousBlockingQueue:同步阻塞队列
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程池 一般用默认即可
- handler:拒绝策略,表示当队列满了并且工作线程大于线程池的最大线程数(maximumPoolSize3)时,如何来拒绝请求执行的Runnable的策略
拒绝策略
以下所有拒绝策略都实现了RejectedExecutionHandler接口
- AbortPolicy:默认,直接抛出RejectedExcutionException异常,阻止系统正常运行
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常,如果运行任务丢失,这是一种好方案
- CallerRunsPolicy:该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务
5.线程池的执行过程
1.在创建了线程池后,等待提交过来的任务请求
2.当调用execute()方法添加一个请求任务时,线程池会做出如下判断
如果正在运行的线程池数量小于corePoolSize,那么马上创建线程运行这个任务
如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列
如果这时候队列满了,并且正在运行的线程数量还小于maximumPoolSize,那么还是创建非核心线程like运行这个任务;
如果队列满了并且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
3.当一个线程完成任务时,它会从队列中取下一个任务来执行
4.当一个线程无事可做操作一定的时间(keepAliveTime)时,线程池会判断:
如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉
所以线程池的所有任务完成后,它会最终收缩到corePoolSize的大小
6.为什么不用默认创建的线程池?
线程池创建的方法有:固定数的,单一的,可变的,那么在实际开发中,应该使用哪个?
我们一个都不用,在生产环境中是使用自己自定义的
为什么不用 Executors 中JDK提供的?
根据阿里巴巴手册:并发控制这章
线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题,如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题
线程池不允许使用Executors去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors返回的线程池对象弊端如下:
FixedThreadPool和SingleThreadPool:
运行的请求队列(LinkedBlockingQueue 链表阻塞队列)长度为:Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
CacheThreadPool和ScheduledThreadPool
运行的请求最大线程数长度为:Integer.MAX_VALUE,线程数上限太大导致OOM
7.生产环境中如何配置 corePoolSize 和 maximumPoolSize
这个是根据具体业务来配置的,分为CPU密集型和IO密集型
CPU密集型
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程)
而在单核CPU上,无论你开几个模拟的多线程该任务都不可能得到加速,因为CPU总的运算能力就那些
CPU密集型任务配置尽可能少的线程数量:
一般公式:CPU核数 + 1个线程数
(为什么是CPU核数 + 1个线程数?)
对于计算密集型的任务,在有N 个处理器的系统上,当线程池的大小为 N+1 时,能实现 CPU 的最优利用率。(即使当计算密集型的线程 偶尔由于页缺失故障或者其他原因暂停时,这个“额外” 的线程也能确保CPU 的时装周期不会被浪费。)
IO密集型
由于IO密集型任务线程并不是一直在执行任务,则可能多的线程,如 CPU核数 * 2
IO密集型,即该任务需要大量的IO操作,即大量的阻塞
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力花费在等待上
所以IO密集型任务中使用多线程可以大大的加速程序的运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集时,大部分线程都被阻塞,故需要多配置线程数:
参考公式:CPU核数 / (1 - 阻塞系数) 阻塞系数在0.8 ~ 0.9左右
例如:8核CPU:8/ (1 - 0.9) = 80个线程数