多线程之线程池使用
常用实现多线程的方式
- 继承 Thread
- 实现 Runable
- 实现 Callable
- 使用ExecutorService、Future实现
为什么要用线程池
- 线程的创建、销毁都是需要消耗资源
- 线程创建的太多,也会导致过度消耗内存,切换时也会浪费大量时间
- 线程池有队列,可以存储待执行的任务
- 线程池可以统一管理线程
- 线程复用,还可以控制最大并发数
JUC
Jdk1.5之后加入了JUC,也就是java.util.concurrent,用官方的解释就是 ‘并发编程中通常有用的实用程序类’ 。java线程池是通过Executor框架实现的,先看Executor和Executors
Executors
先看Executors它里面有这些方法
其中常用的线程池有
newSingleThreadExecutor 、
newFixedThreadExecutor、
newCachedThreadExecutor、
newScheduledThreadPool
这四种有兴趣的同学可以看一下底层源码,但我们只需要弄清他的原理不建议使用(原因看阿里巴巴编程规范),直击底层会发现这些线程池最终都是调用
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
这是ThreadPoolExecutor类的构造方法,记住里面的七个参数,后面会详细注明每个参数的含义及运行的原理。而ThreadPoolExecutor类的顶级接口就是Executor,也就是我们接下来要说的。
Executor
Executor是线程池的顶级接口,但他却不是线程池,只是个执行线程的工具。我们先看看官方API文档怎么说的,
我们已知的子接口:
ExecutorService, ScheduledExecutorService
所有已知的实现类:
AbstractExecutorService,
ForkJoinPool(将大型任务分解为小任务的线程池),
ScheduledThreadPoolExecutor(支持定时调度的线程池),
ThreadPoolExecutor。
今天我们的主讲ThreadPoolExecutor。。
线程池的组成
线程池有四个部分组成:
- 线程池管理器,用于创建并管理线程
- 工作线程,线程池内的线程
- 任务接口:每个任务需实现的接口,永无线程调度及运行
- 任务队列:存放待处理的任务,提供缓存
线程池的状态
private static final int COUNT_BITS = Integer.SIZE - 3;//workerCount所占位数
private static final int CAPACITY = (1 << COUNT_BITS) - 1;//workerCount上线
// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;//正常接收任务,正常处理阻塞队列里的任务
private static final int SHUTDOWN = 0 << COUNT_BITS;//不会接收任务,会执行完正在执行的任务,正常处理阻塞队列里的任务
private static final int STOP = 1 << COUNT_BITS;//不会接收任务,会中断正在执行的任务,会放弃处理阻塞队列里的任务
private static final int TIDYING = 2 << COUNT_BITS;//任务全部执行完毕,当前活动线程是0,即将进入终结
private static final int TERMINATED = 3 << COUNT_BITS;//终结状态
ThreadPoolExecutor
我们看ThreadPoolExecutor类里面有四个构造方法,主要看
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,TimeUnit unit,BlockingQueue workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)
我们先来看看这个构造方法里面的七个参数的详细解释:
- corePoolSize: 核心线程数,指保留的线程池中线程数量的大小(不超过maximumPoolSize值 时,线程池中最多有corePoolSize 个线程工作)。
- maximumPoolSize : 指的是线程池的最多能拥有的线程数量(超过这个数量,任务将会存放指定队列)
- keepAliveTime : 指的是空闲线程结束的超时时间(超过corePoolSize部分的线程不工作时,过keepAliveTime 时间将停止该线程)。
- unit: 是一个枚举,表示 keepAliveTime 的单位(有NANOSECONDS, MICROSECONDS,MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS,7个可选值)。
- workQueue : 表示存放任务的队列(存放需要被线程池执行的线程队列)。
- threadFactory: 执行程序创建新线程时使用的工厂。
- handler: 拒绝策略,由于超出线程范围和队列容量而使执行被阻塞时所使用的处理程序。
线程池工作过程
线程池是如何工作的呢,我们来好好整整ThreadPoolExecutor工作过程及七个参数何时用怎么用。
线程池刚创建是没有线程的,新任务到来不会立马启动,只有当调用execute() 方法添加一个任务时,线程池就会判断核心线程数corePoolSize是否已经达最大值,没有则创建线程执行任务,大于corePoolSize时就将任务塞到队列(workQueue )等待执行,当队列被塞满,会再次创建非核心线程执行任务,直到线程数达到maximumPoolSize,再进入的任务则会被拒绝策略 RejectedExecutionHandler 处理。线程会不断从队列中获取任务执行直到队列为空,有线程超过keepAliveTime 时间未工作则回收,直到只剩核心线程数工作。
举个生活例子解释,银行总共有10个窗口(maximumPoolSize),大厅有座位(workQueue ),银行平时固定开5个窗口(corePoolSize)办事,当5个窗口都被占满时,剩余的客人就只能在座位上等待,座位坐满了银行经理会将剩下的5个窗口也安排人办理事情,当10个窗口满了,座位也满了,这时候就让保安(RejectedExecutionHandler )安排后面来的客人。慢慢座位空了,窗口也逐渐开始没人,窗口长时间没人时银行经理就会安排减少窗口,直到只剩固定的五个窗口。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
//工作线程数小于核心线程数,则调用addWorker创建线程工作
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 如果已开启的工作线程数大于或等于核心线程数,则把任务添加入堵塞队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 添加失败,则继续开启工作线程,如果已经开启线程数小于最大线程数,则添加成功,否则,使用指定的拒绝策略拒绝
else if (!addWorker(command, false))
reject(command);
}
/**
* 提交Runnable,指定返回值
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
/**
* 提交Runnable,指定返回值
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}
/**
* 提交Callable
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
ThreadPoolExecutor参数的设置选择
其实关于参数的选择,ThreadPoolExecutor源码是有介绍的。
线程数大小的选择:
- 纯计算的任务多建议线程数为CPU数量或加一;
- 任务包含大量IO/网络等待等,线程数 = CPU 核数 × 目标 CPU 利用率 ×(1 + 平均等待时间 / 平均工作时间)
队列的选择:
- ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
- LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
- SynchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
这是三个阻塞队列,有兴趣可以了解下java的其他阻塞队列和非阻塞队列
拒绝策略 RejectedExecutionHandler的选择:
public enum RejectPolicy {
/** 处理程序遭到拒绝将抛出RejectedExecutionException */
ABORT(new ThreadPoolExecutor.AbortPolicy()),
/** 放弃当前任务 */
DISCARD(new ThreadPoolExecutor.DiscardPolicy()),
/** 如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程) */
DISCARD_OLDEST(new ThreadPoolExecutor.DiscardOldestPolicy()),
/** 由主线程来直接执行 */
CALLER_RUNS(new ThreadPoolExecutor.CallerRunsPolicy());
private RejectedExecutionHandler value;
private RejectPolicy(RejectedExecutionHandler handler) {
this.value = handler;
}
/**
* 获取RejectedExecutionHandler枚举值
*
* @return RejectedExecutionHandler
*/
public RejectedExecutionHandler getValue() {
return this.value;
}
}
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:丢弃无法处理的任务,也不抛异常。如果允许丢弃任务,这事最好的方案。
- ThreadPoolExecutor.DiscardOldestPolicy:也是丢弃任务,只不过是丢弃即将被执行的任务,并提交当前任务
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
- 若以上策略还不能满足需要,可以自己扩展 RejectedExecutionHandler 接口
工厂(threadFactory)的选择:
如果不是特别指定就选择默认的工厂,还有一个privilegedThreadFactory,返回用于创建新线程的线程工厂,这些新线程与当前线程具有相同的权限
public static ThreadFactory defaultThreadFactory() {
return new DefaultThreadFactory();
}
public static ThreadFactory privilegedThreadFactory() {
return new PrivilegedThreadFactory();
}
最后
其实想要玩转多线程,还得了解线程的三大特性,可以看看JMM(java内存模型),volatile关键字的作用,还有线程锁、线程调度等等。java api文档下的这三个包里面的类都可以看看