线程池

多线程的软件设计方法确实可以最大限度的发挥现代多核处理器的计算能力,提高生产系统的吞吐量和性能。但是,若不加控制和管理的随意使用线程,对系统的性能反而会产生不利的影响。

  首先,虽然与进程相比,线程是一种轻量级的工具,但是其创建和关闭依然需要花费时间,如果为每一个小的任务都创建一个线程,很有可能出现创建和销毁线程所占的时间大于该线程真实工作所消耗的时间的情况,反而得不偿失;

  其次,线程本身也是要占用内存空间的,大量的线程会抢占宝贵的内存资源,如果处理不当,可能会导致Out Of Memory异常;即使没有,大量的线程回收也会给GC带来很大的压力,延长GC的停顿时间。因此,对线程的使用必须掌握一个度,在有限的范围内,增加线程的数量可以明显提高系统的吞吐量,但一旦超出了这个范围,大量的线程只会拖垮应用系统;

  所以,在实际的生产环境中,线程的数量必须得到控制,盲目的大量创建线程对系统性能是有伤害的。

  为了避免系统频繁的创建和销毁线程,我们可以让创建的线程进行复用。线程池就具备这样的功能,线程池中,总有那么几个活跃的线程,当你需要使用线程时,可以从池子中随便拿一个空闲的线程,当完成工作时,并不着急关闭线程,而是将这个线程退回到池子中,方便其他人使用。也就是说,使用线程池后,创建线程就变成了从线程池中获得空闲线程,关闭线程变成了向池子中归还线程。

JDK对线程池的支持

为了更好的控制多线程,JDK提供了一套Executor框架,其本质就是一个线程池,它的核心成员如下图所示:

   以上的成员均在java.util.concurrent包中,是JDK并发包的核心类。其中的ThreadPoolExecutor表示一个线程池,Executors类则扮演着线程池工厂的角色,通过Executors可以取得一个拥有特定功能的线程池。从上UML图中也可看出,ThreadPoolExecutor类实现了Executor 接口,因此,通过这个接口,任何的Runnable 的对象都可以被 ThreadPoolExecutor 线程池调度。

   Executor 框架提供了多种类型的线程池,主要有以下方法:

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)方法:该方法返回一个固定线程数量的线程池。该线程池的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行;若没有,则新的任务会暂时保存在一个队列里,待有线程空闲的时候,便处理在任务队列里的任务。

  ❤ newSingleThreadExecutor()方法:该方法返回只有一个线程的线程池。若多于一个任务被提交到该线程池,任务会被保存在一个任务队列里,待线程空闲,按先入先出的顺序执行队列中的任务。

  ❤ newCachedThreadPool()方法:该方法返回一个可根据实际情况调整线程数量的线程池。线程池中的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有的线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务完成后,将返回线程池进行复用。

  ❤ newSingleThreadScheduledExecutor()方法:该方法返回一个ScheduledExecutorService 对象,线程池大小为1。ScheduledExecutorService 接口在ExecutorService 接口之上扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行,或者周期性的某个任务。

  ❤newScheduledThreadPool(int corePoolSize)方法:该方法返回一个 ScheduledExecutorService 对象,但该线程池可以指定线程的数量。

下面以newFixedThreadPool()为例,展示一下线程池的使用:

 1 public  class  ThreadPool {
 2     public static class myTask implements Runnable{
 3         @Override
 4         public void run() {
 5             System.out.println(System.currentTimeMillis() + ": Thread ID :" + Thread.currentThread().getId());
 6             try {
 7                 Thread.sleep(1000);
 8             } catch (InterruptedException e) {
 9                 e.printStackTrace();
10             }
11         }
12     }
13     //测试
14     public static void main(String[] args){
15         myTask task = new myTask();
16         ExecutorService es = Executors.newFixedThreadPool(5);
17         for (int i = 0;i < 10;i++){
18             es.submit(task);
19         }
20         es.shutdown();
21     }
22 }

输出结果:

1537949947330: Thread ID :11
1537949947330: Thread ID :14
1537949947330: Thread ID :13
1537949947330: Thread ID :12
1537949947330: Thread ID :15
1537949948331: Thread ID :13
1537949948331: Thread ID :12
1537949948331: Thread ID :15
1537949948331: Thread ID :11
1537949948331: Thread ID :14

上述代码中,在第16行创建了固定大小的线程池,内有5个线程,在第18行,依次向线程池中提交10个任务,线程池会安排调度这10个任务。

从输出结果来看,很兼容前5个任务和后5个任务的执行时间正好相差1S,并且这前后5个线程的ID也是一致的。这说明了这10个任务是分成2批执行的,完全符合newFixedThreadPool线程池的行为。

计划任务(newScheduledThreadPool(int corePoolSize))

newScheduledThreadPool(int corePoolSize)方法与其它的方法不太相同,newSingleThreadScheduledExecutor()方法内部也是采用newScheduledPool(1)来实现的。newScheduledThreadPool(int corePoolSize)返回一个 ScheduledExecutorService 对象,可以根据时间需要对线程进行调度,它的一些主要方法如下:

1 public ScheduledFuture<?> scheduled(Runnable command,long delay,TimeUnit unit);
2 public ScheduledFuture<?> scheduleAtFixedRate(Runnable command ,long initialDelay,long period,TimeUnit unit);
3 public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit);

ScheduledExecutorService 并不一定会立即安排执行任务,它其实是起到了计划任务的作用,它会在指定的时间,对任务进行调度。上面的三个方法,scheduled()方法会在给定时间,对任务进行一次调度;另外的两个方法会对任务进行周期性的调度,但这两个方法有一定的区别:

  scheduledAtFixedRate()方法:任务调度的频率是一定的。它是以上一个任务开始执行时间为起点,之后以period为周期,调度下一次的任务;

  scheduledWithFixedDelay()方法:它是以上一个任务执行结束后为起点,再经过period时间周期,进行下一次的任务调度;

下面例子使用scheduledAtFixedRate()方法调度任务:

 1 public class ScheduledExecutorServiceDemo {
 2     public static void main(String[] args){
 3         ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);
 4         //如果前面的任务没有完成,则调度也不会启动
 5         ses.scheduleAtFixedRate(new Runnable() {
 6             @Override
 7             public void run() {
 8                 try {
 9                     Thread.sleep(1000);
10                     System.out.println(System.currentTimeMillis() / 1000);
11                 } catch (InterruptedException e) {
12                     e.printStackTrace();
13                 }
14             }
15         },0,2, TimeUnit.SECONDS);
16     }
17 }

输出结果:

1537952845
1537952847
1537952849
1537952851
1537952853

输出的单位为S,可以看出时间间隔为2S。符合我们预期。

设想:若执行任务的时间大于调度时间的间隔,会发生什么情况呢?会不会出现任务堆叠的情况呢?

将上述代码第9行,修改为Thread.sleep(8000),再运行代码,结果:

1537953112
1537953120
1537953128
1537953136
1537953144
1537953152
1537953160

由输出结果看出,时间间隔为8S,也就是说ScheduledExecutorService不会让任务出现堆叠的情况,同时,周期如果太短,那么任务会在上一个任务结束后,立即执行。

上述代码如果采用scheduleWithFixedDelay()方法来调用任务,并且照例修改为8秒,那么任务的调度实际间隔将是10秒,大家可以自行测试一下。

注意:

  调度任务并不会保证无限期的持续调用,如果任务本身抛出了异常,那么后续的所有执行都会被中断,因此,必须保证异常被及时处理,为周期性的任务稳定调度提供条件。

 参考:《Java高并发程序设计》 葛一鸣 郭超 编著:

作者:Joe
努力了的才叫梦想,不努力的就是空想,努力并且坚持下去,毕竟这是我相信的力量
原文地址:https://www.cnblogs.com/Joe-Go/p/9707435.html