java线程池
Java标准库提供了java.util.concurrent.ExecutorService
接口表示线程池,并提供了几个实现,通过java.util.concurrent.Executors类提供的方法可以创建线程池,例如:
- FixedThreadPool:线程数固定的线程池;
- CachedThreadPool:线程数根据任务动态调整的线程池;
- SingleThreadExecutor:仅单线程执行的线程池。
以上都有其局限性,不够灵活;另外这几种方法内部也是通过java.util.concurrent.ThreadPoolExecutor方式实现,使用java.util.concurrent.ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。
线程池参数
corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;
maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;
keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;
unit:keepAliveTime的单位
workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列、有界任务队列、无界任务队列、优先任务队列几种;
任务队列:
直接提交队列:为SynchronousQueue队列,SynchronousQueue是一个特殊的BlockingQueue,它没有容量,一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。
有界的任务队列:使用ArrayBlockingQueue实现,是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
无界的任务队列:使用LinkedBlockingQueue实现,一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。
优先任务队列:通过PriorityBlockingQueue实现,可以自定义规则根据任务的优先级顺序先后执行。
threadFactory:线程工厂,用于创建线程,一般用默认即可;
handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务;
拒绝策略
AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作;
CallerRunsPolicy策略:如果线程池的线程数量达到上限,该策略会把任务队列中的任务放在调用者线程当中运行;
DiscardOledestPolicy策略:该策略会丢弃任务队列中最老的一个任务,也就是当前任务队列中最先被添加进去的,马上要被执行的那个任务,并尝试再次提交;
DiscardPolicy策略:该策略会默默丢弃无法处理的任务,不予任何处理。当然使用此策略,业务场景中需允许任务的丢失;
以上内置的策略均实现了RejectedExecutionHandler接口,当然你也可以自己扩展RejectedExecutionHandler接口,定义自己的拒绝策略。
线程池的扩展
ThreadPoolExecutor扩展主要是围绕beforeExecute()、afterExecute()和terminated()三个接口实现的,
beforeExecute:线程池中任务运行前执行
afterExecute:线程池中任务运行完毕后执行
terminated:线程池退出后执行
通过这三个接口我们可以监控每个任务的开始和结束时间,或者其他一些功能。下面我们可以通过代码实现一下:
public class ThreadPool {
private static ExecutorService pool;
public static void main( String[] args ) throws InterruptedException
{
//实现自定义接口
pool = new ThreadPoolExecutor(2, 4, 1000, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(5),
new ThreadFactory() {
public Thread newThread(Runnable r) {
System.out.println("线程"+r.hashCode()+"创建");
//线程命名
Thread th = new Thread(r,"threadPool"+r.hashCode());
return th;
}
}, new ThreadPoolExecutor.CallerRunsPolicy()) {
protected void beforeExecute(Thread t,Runnable r) {
System.out.println("准备执行:"+ ((ThreadTask)r).getTaskName());
}
protected void afterExecute(Runnable r,Throwable t) {
System.out.println("执行完毕:"+((ThreadTask)r).getTaskName());
}
protected void terminated() {
System.out.println("线程池退出");
}
};
for(int i=0;i<10;i++) {
pool.execute(new ThreadTask("Task"+i));
}
pool.shutdown();
}
}
public class ThreadTask implements Runnable{
private String taskName;
public String getTaskName() {
return taskName;
}
public void setTaskName(String taskName) {
this.taskName = taskName;
}
public ThreadTask(String name) {
this.setTaskName(name);
}
public void run() {
//输出执行线程的名称
System.out.println("TaskName"+this.getTaskName()+"---ThreadName:"+Thread.currentThread().getName());
}
}
线程池的参数设置
线程池线程数量的设置没有一个明确的指标,根据实际情况,只要不是设置的偏大和偏小都问题不大,结合下面这个公式即可
/**
* Nthreads=CPU数量
* Ucpu=目标CPU的使用率,0<=Ucpu<=1
* W/C=任务等待时间与任务计算时间的比率
*/
Nthreads = Ncpu*Ucpu*(1+W/C)
线程池的关键参数
corePoolSize
maximumPoolSize
keepAliveTime
workQueue capacity
以上几个参数的值,会对使用者有很大的影响,对线程池的调优,也就是围绕这几个参数来设置的。
动态修改参数的线程池
正常情况下,我们使用线程池,都是在第一次初始化时,设置好了以上的这几个关键参数,如果后续想对这几个参数进行调整,都需要重启应用,所以,需要一种可以在程序运行期间可以修改这几个参数的线程池,其实线程池本身corePoolSize、maximumPoolSize和keepAliveTime这几个参数是支持动态修改的,唯一不能改的是队列的长度,因为这个是阻塞队列本身决定的,是在创建队列时,构造函数传进去的,看java.util.concurrent.LinkedBlockingQueue这个队列:
/** The capacity bound, or Integer.MAX_VALUE if none */
private final int capacity;
这个容量是final的,所以,需要重写一个队列,也就说,也需要重写一个线程池,使用重写的队列。