多线程的使用
参考:
多线程、线程池的创建方式,为什么阿里推荐自定义线程池 completablefuture 自定义线程池
多线程4种实现方式
1、继承Thread类
2、实现Runnable接口
3、实现Callable接口
- 实现Callable不能简单把Callable对象传给thread,要使用FutureTask做一次封装
- get()可以获取到call()返回值
- get()可以获取到call()返回值
4、线程池
通常在业务代码中前三种都不使用,只使用第四种(线程池)
每个系统通常有一两个线程池,每一个异步任务,提交给线程池执行即可
提交任务到线程池的两种方式
①execute() 返回值为void,代表只执行异步任务,没有返回值
②submit() 返回值为Future,既可以执行任务,野口接收返回值。
四种创建线程方式的区别
①:继承thread和实现runnable接口的方式,无法得到返回值,实现callable接口可以得到返回值。
②:前三种方式都无法控制资源,即来一个线程就要创建一个线程,容易使系统资源耗尽
③:线程池的方式可以控制资源,系统性能稳定。
线程池的优点
①:降低资源消耗。可以重复利用已经创建好的线程,降低线程的创建和销毁带来的损耗。
②:提高响应速度。因为线程池中的线程都处于等待分配任务状态,当任务过来时可以直接执行。
③:提高线程的可管理性。如果是单cpu的话,创建多个线程,会导致资源耗尽,但是线程池有拒绝策略;另外还可以核心业务和非核心业务两种线程池,如果某个时间内存压力大,可以释放掉非核心业务线程池。使用线程池就可以使线程的管理方便。
使用ThreadPoolExecutor方式创建线程池
ThreadPoolExecutor属于原生的创建方式,其他的线程池创建方式都是封装了ThreadPoolExecutor类。
ThreadPoolExecutor继承关系图下图
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}
创建ThreadPoolExecutor 线程池的时候有七个重要参数
- int corePoolSize:核心线程数,线程池创建好就已经准备就绪的线程数 量,等待接收异步任务 ,异步任务进来后,自动执行。核心线程会一直存在,除非设置了allowCoreThreadTimeOut,才允许核心线程超时。
- int maximumPoolSize:线程池允许存在的最大线程数
- long keepAliveTime:超时时间。如果当前线程数量大于核心数量,且在keepAliveTime时间内保持空闲,就释放掉。 释放的是最大线程数 - 核心线程数
- TimeUnit unit: 超时时间单位
- BlockingQueue workQueue:阻塞队列,如果线程有很多,就会把线程保存在队列里 只要线程有空闲,就去阻塞队列中取。
- ThreadFactory threadFactory:线程的创建工厂
- RejectedExecutionHandler handler:拒绝策略 ,如果线程满了采取的策略
拒绝策略分类:
①:DiscardOldestPolicy 抛弃掉最早进入的线程
②:DiscardPolicy 抛弃掉最新的线程
③:DiscardPolicy 剩余的线程调用run方法,变为同步执行
④:DiscardPolicy 抛弃掉最新的线程,并抛出异常!
使用Executors创建线程
Executors创建线程,其底层还是用的是ThreadPoolExecutor创建的
可以看到 Executors 的源码实现
-
newFixedThreadPool(int nThreads)
固定大小 core = 自定义的线程数,但阻塞队列是无界队列,会
OOM
内存溢出核心线程数和最大线程数相等;
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
-
newCachedThreadPool()
core是0,最大线程数无限大,无限创建线程的话,会使
cpu
占用100%
,因为cpu要不停的调度线程去执行任务
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- newSingleThreadExecutor(); 单线程的线程池,后台从队列里取,挨个执行。阻塞队列是无界队列,会
OOM
内存溢出
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
- newScheduledThreadPool(); 带有定时任务的线程池
public static ScheduledExecutorService newScheduledThreadPool(
int corePoolSize, ThreadFactory threadFactory) {
return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
线程池的工作顺序
- 线程池创建,准备好核心线程数的线程,准备接收任务
- 任务越来越多,执行任务的线程数达到核心线程数,这时候进来的新任务会存在阻塞队列中
- 当核心线程执行完后就去队列中执行任务
- 阻塞队列满了,就会开启新的线程,直到线程数达到最大线程数
- 注意:此时队列满了,新创建的线程会优先处理新进来的任务,而不是处理队列里的任务,
- 队列里的任务只能由核心线程忙完了再来执行,可能导致队列的任务等待时间过长,队列积压,特别是IO密集型的场景
- 达到最大线程数后,任务还在进来,就会执行拒绝策略
- 当任务减少,当前线程池的线程数大于核心线程数,超过keepAliveTime 后,max-core的线程会被回收内存空间;
阻塞队列
为什么要使用阻塞队列?
队列是先进先出,当放入一个元素,会放在队列的尾部,而取出一个元素是在队列头部取出,那么当队列为空或队列满了怎么办?
使用阻塞队列,当队列为空,取元素会被阻塞,当队列满了,添加新元素也会被阻塞;
等空队列有数据,或满的队列有空余容量了,被阻塞的线程就会被自动唤醒;
好处是:
不需要关注队列何时阻塞,阻塞的线程何时唤醒,这都是有阻塞队列来完成;