Java中的线程池
【1】使用线程池的好处:
1)降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
2)提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。
3)提高线程的可管理性。
说明:不同的业务,我们一般会使用不同的线程池,以便于对同一业务的线程进行统一管理:(日志)监控、线程池参数调优。
【2】构造方法:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler);
参数说明:
1)corePoolSize:核心池的大小。
1)在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务。
2)默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务。
注意:即使其它空闲的线程能够执行新任务也会去创建线程,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
3)调用prestartAllCoreThreads()或prestartCoreThread()方法来预创建线程,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。
2)workQueue:任务缓存队列,是一个阻塞队列,用来存储等待执行的任务;类型为BlockingQueue<Runnable>,通常可以取下面4种类型:
1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE,吞吐量通常要高于ArrayBlockingQueue。
eg:静态工厂方法Executors.newFixedThreadPool()就是使用的这个队列。
3)SynchronousQueue:这个队列不会保存提交的任务,而是直接新建一个线程来执行新来的任务。
说明:每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。
eg:静态工厂方法Executors.newCachedThreadPool使用的就是这个队列。
4)PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
说明:
1>一般使用LinkedBlockingQueue和SynchronousQueue
2>建议使用有界队列,有界队列能增加系统的稳定性。
eg:如果线程池里的工作线程全部阻塞,任务积压在线程池里,如果设置成无界队列,那么这个队列会越来越大,有可能会撑满内存,导致整个系统不可用。
3)maximumPoolSize:线程池中允许创建的最大线程数。
1)如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程来执行任务。
2)如果使用了无界的任务队列,则这个参数就不起什么作用了(队列默认的大小是:Integer.MAX_VALUE,在队列未满之前,线程池是不会再去创建新线程了)。
4)RejectedExecutionHandler:任务拒绝策略,当任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略,通常有以下四种策略:
1)ThreadPoolExecutor.AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常,默认使用该策略。
2)ThreadPoolExecutor.DiscardPolicy: 丢弃任务,但是不抛出异常;会导致被丢弃的任务无法再次被执行
3)ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程);会导致被丢弃的任务无法再次被执行
4)ThreadPoolExecutor.CallerRunsPolicy: 由调用线程处理该任务;主线程直接执行该任务,执行完之后尝试添加下一个任务到线程池中,可以有效降低向线程池内添加任务的速度
说明:
1>我们也可以实现RejectedExecutionHandler接口来自定义任务拒绝策略。
2>我们也可以在ThreadPoolExecutor外面包装一层,在wrapper中自定义更多的场景:
eg:在执行自定义的execute方法时,当抛出RejectedExecutionException异常时,我们可以根据传入的参数(一般通过注解的方式来传参)来决定采取什么策略。
3>RejectedExecutionHandler的设计属于设计模式中的策略模式。
5)keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。
默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用。
但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;
6)TimeUnit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:
TimeUnit.DAYS; 天
TimeUnit.HOURS; 小时
TimeUnit.MINUTES; 分钟
TimeUnit.SECONDS; 秒
TimeUnit.MILLISECONDS; 毫秒
TimeUnit.MICROSECONDS; 微妙
TimeUnit.NANOSECONDS; 纳秒
7)ThreadFactory:创建线程的工厂
说明:
1>我们可以自定义线程工厂,以便给每个线程设置更有意义的名字。
2>可以通过修改java.util.concurrent.Executors的内部类DefaultThreadFactory来自定义线程工厂。
【3】其它方法:
任务的提交:
execute(Runnable command): 提交后没有返回值,故无法判断任务是否被线程池执行成功。
submit(): 提交后返回一个Future对象,通过这个future对象可以判断任务是否执行成功。
通过future的get()方法来获取返回值,该方法会阻塞当前线程直到任务完成。
get(long timeout, TimeUnit unit)方法:阻塞当前线程一段时间后立即返回,这时候任务可能没有执行完。
线程池容量的动态调整:
setCorePoolSize() 设置核心池的大小
setMaximumPoolSize() 设置线程池最大能创建的线程数
线程池的关闭:
原理:遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。
shutdown()
1>将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
2>不再接受新的任务,但是不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止。
shutdownNow()
1>首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并且清空任务缓存队列,并返回等待执行任务的列表.
说明:
1)只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。
2)当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。
【4】线程池的状态:
// RUNNING状态:线程池正常运行,可以接受新的任务并处理队列中的任务
private static final int RUNNING = -1 << COUNT_BITS;
// SHUTDOWN状态:不再接受新任务,但是会执行队列中的任务
private static final int SHUTDOWN = 0 << COUNT_BITS;
// STOP状态:不再接受新任务,不处理队列中的任务,中断正在处理的任务
private static final int STOP = 1 << COUNT_BITS;
// 过渡状态:所有的任务都执行完了,线程池已经没有有效的线程了,此时线程池的状态为过渡状态,并且将要调用terminated()方法
private static final int TIDYING = 2 << COUNT_BITS;
// 终止状态:terminated()方法调用完成后的状态
private static final int TERMINATED = 3 << COUNT_BITS;
1>当线程池刚创建后,线程池处于RUNNING状态,可以接受新的任务并处理队列中的任务
2>如果调用了shutdown()方法,则线程池处于SHUTDOWN状态,此时线程池不能够接受新的任务,它会等待所有任务执行完毕;
3>如果调用了shutdownNow()方法,则线程池处于STOP状态,此时线程池不能接受新的任务,并且会去尝试终止正在执行的任务;
4>当线程池处于SHUTDOWN或STOP状态,并且所有工作线程已经销毁,任务缓存队列已经清空或执行结束后,线程池被设置为TERMINATED状态。
【5】线程池的处理流程:
ThreadPoolExecutor执行execute方法分下面4种情况。
1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。
2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。
3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理该任务。
4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用 RejectedExecutionHandler.rejectedExecution()方法。
源码(jdk8):
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) { // 1)线程数小于corePoolSize,创建线程并立即执行任务。
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) { // 2)线程数达到corePoolSize,任务被添加到队列中。
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false)) // 3)队列已满,则创建新线程并立即执行该任务。
reject(command); // 4)线程数已达到maximumPoolSize,创建线程失败,执行拒绝策略。
}