1.线程池的使用
Exector框架提供了各种类型的线程池,主要有以下几种方法:
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newCachedThreadPool()
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
- newFixedThreadPool(int nThreads): 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行;如果没有,则新的任务会被暂存在一个任务队列(LinkedBlockingQueue有序)中,等到有线程空闲的时候,便处理任务队列中的任务。
2.newSingleThreadExecutor():该方法返回一个只有一个线程的线程池。若多余一个任务被提交到线程池,任务会被保存到一个任务队列(LinkedBlockingQueue)中,等到线程空闲,按先入先出的顺序执行队列中的任务。
3.newCachedThreadPool() :该方法返回一个可根据实际情况调整线程数量的线程池。线程池中的数量不确定,但若有空闲的线程可以复用,则优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,返回线程池进行复用。
4.newSingleThreadScheduledExecutor() :该方法返回一个ScheduledExecutorService对象,线程池的大小为1。ScheduledExecutorService接口在ExecutorService接口之上扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务。
5.newScheduledThreadPool(int corePoolSize):该方法也返回一个ScheduledExecutorService对象,但可以指定线程的数量。
固定大小线程池使用实例
public class FixedThreadPoolDemo {
public static void main(String[] args) {
ExecutorService es = Executors.newFixedThreadPool(5);
for (int i=0;i<5;i++){
es.submit(()->{
System.out.println(System.currentTimeMillis()+"ThreadID:"+Thread.currentThread().getId());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
}
计划任务
另一个值得注意的方法是newScheduledThreadPool(),它返回一个ScheduledExecutorService对象,可以根据时间需要对线程进行调度。它的一些方法如下:
public ScheduledFuture<?> schedule(Runnable command,
long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
schedule会在给定时间,对任务进行一次调度。
scheduleAtFixedRate任务调度的频率是一定的,它是以上一个任务开始执行时间为起点,在之后的period时间调度下一次任务。
scheduleWithFixedDelay以上一次任务结束以后的时间为起点,再经过delay时间进行任务调度。
实例代码
public class ScheduleAtFixedRate {
public static void main(String[] args) {
ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);
ses.scheduleAtFixedRate(()->{
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getId()+System.currentTimeMillis()/1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
},0,2, TimeUnit.SECONDS);
}
}
注意:如果任务的执行时间超过调度时间的话,比如调度周期是2S,任务执行时间是8S,那么并不会出现多个任务堆叠到一起的情况,如果将上述代码的Thread.sleep改为8000,那么任务的执行周期就不是2S,而是8S。
2.核心线程池的内部实现
无论是newFixedThreadPool方法还是newSingleThreadExecutor方法还是newCachedThreadPool方法,其内部实现均使用了ThreadPoolExecutor类。
下面给出这三个线程池的实现方式
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
创建线程池主要通过ThreadPoolExecutor类完成。ThreadPoolExcetor有很多重载方法,通过参数最多的构造方法来理解创建线程池需要配置那些参数,构造方法如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1.corePoolSize:指定了线程池中的线程数量。
2.maximumPoolSize:指定了线程池中的最大线程数量。。
3.keepAliveTime:空闲线程存活时间。如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗。
4.unit:为keepAliveTime指定时间单位
- workQueue:任务队列。被提交但尚未执行的任务。
6.threadFactory:创建线程的工厂类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。一般用默认的就行。
7.handler:拒绝策略,当任务太多来不及处理时,如何拒绝任务。
workQueue和handler详解
workQueue
参数workQueue指被提交但未执行的任务队列,它是一个BlockingQueue接口的对象,仅用于存放Runnable对象。可以使用以下几种:
1.直接提交队列(SynchronousQueue)
SynchronousQueue是一种特殊的BlockingQueue。SynchronousQueue没有容量,每一个插入操作都要等待一个响应的删除操作,反之,每一个删除操作都要等待对应的插入操作。如果使用SynchronousQueue,提交的任务不会被真实保存,而总是将新任务提交给线程执行,如果没有空闲线程,则尝试创建新的线程,则尝试创建新的线程,如果线程数量达到最大值,则执行拒绝策略。因此,使用SynchronousQueue队列,通常要设置很大的maxumumPoolSize值,否则很容易执行拒绝策略
2.有界的任务队列(ArrayBlockingQueue)
ArrayBlockingQueue类的构造函数必须带一个容量参数,表示该队列的最大容量
public ArrayBlockingQueue(int capacity)
当使用有界的任务队列的时候,若有新的任务需要执行,如果线程池的实际线程数小于corePoolSize,则会优先创建新的线程;若大于corePoolSize,则会将新任务加入等待队列。若等待队列已满,无法加入,则在总线程数不大于maximumPoolSize的前提下,创建新的线程执行任务。若大于maximumPoolSize,则执行拒绝策略。可见,有界队列仅当任务队列装满时,才可能将线程数提升到corePoolSize以上。也就是说除非系统特别繁忙,否则要确保核心线程数维持在corePoolSize。
3.无界的任务队列 (LinkedBlockingQueue)
与有界任务队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来时,系统线程数小于corePoolSize时,线程池会生成新的线程执行任务,当系统的线程数达到corePoolSize后,就不会继续增加了。若后续仍有新的任务加入,又没有空闲的线程资源,则任务直接进入队列等待,若任务创建和处理的速度差异很大,无界队列会保持一直增长,直到资源耗尽。
4.优先任务队列(PriorityBlockingQueue)
可以控制任务的执行先后顺序。它是一个特殊的无界队列。PriorityBlockingQueue类可以根据任务自身的优先级顺序先后执行,在确保系统性能的同时,也有很好额质量保证。
回顾newFixedThreadPool方法的实现,它返回了一个corePoolSize和maximumPoolSize大小一样的,并且使用LinkedBlockingQueue任务队列的线程池。对于固定大小的线程池而言,不存在线程数量动态变化,因此corePoolSize和maximumPoolSize大小相等,它使用无界队列存放无法执行的任务,当任务提交频繁时,该队列迅速增长,从而导致系统资源耗尽。
newSingleThreadExecutor方法返回的是单线程线程池,是newFixedThreadPool方法的一种退化,只是将线程池线程数量设为1。
newCachedThreadPool方法的corePoolSize为0,而maximumPoolSize为无穷大的线程池,这意味着没有任务时,该线程池内无线程,而当任务被提交时,该线程池会使用空闲线程执行任务,如果没有空闲线程,则将任务加入SynchronousQueue队列,而SynchronousQueue队列是一种直接提交队列,它会迫使线程池增加新的线程执行任务。当任务执行完毕后,由于corePoolSIze为0,空闲线程又会在指定60S被回收。
对于newChchedThreadPool方法,如果同时有大量任务被提交,而任务的执行又不是很快时,系统会开启等量的线程处理,这样很快会耗尽系统资源。
ThreadPoolExecutor线程池的核心调度代码如下
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
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);
}
handler
handler指定了拒绝策略,也就是当任务数量超过系统实际承载能力时,就要用到拒绝策略。拒绝策略可以说是系统超负荷运行时的补救措施,通常由于压力太大而引起的。也就是当等待队列已满并且当前线程数量超过maximumPoolSize时,处理
的策略。
JDK内置4种策略
AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。
CallerRunsPolicy策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务,显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能急速下降。
DiscardOldestPolicy策略:该策略丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务
DiscardPolicy策略:该策略默默丢弃无法处理的任务,不予任何处理。如果允许任务丢弃,这是最好的方案。