Java 线程池

本文主要介绍如何开始创建线程以及管理线程池,在 Java 语言中,一个最简单的线程如下代码所示:

Runnable runnable = new Runnable(){
    public void run(){
        System.out.println("Run");
    }
}
//启动线程
new Thread(runnable).start();

这是一个再简单不过的例子了,但如果有许多需要长时间运行的任务同时执行,并需要等所有的这些线程都执行完毕,还想得到一个返回值,那么这就有点小小难度了。但Java已经有解决方案给你(JDK1.5之后),那就是 Executors,一个简单的可以让你创建线程池和线程工厂。

Executor与Executors的区别

Executor 是Java线程池的顶级接口,可以理解为表示Java线程池的概念
Executors 是一个类,提供若干个静态方法用于创建不同类型的线程池线程池使用 ExecutorService(接口) 的实例来表示,通过 ExecutorService 可以提交任务,并进行调度执行。

Executors创建线程池的方式

根据返回的对象类型创建线程池可以分为三类:

  • 创建返回ThreadPoolExecutor对象

  • 创建返回ScheduleThreadPoolExecutor对象

  • 创建返回ForkJoinPool对象

ThreadPoolExecutor

在介绍Executors创建线程池方法前先介绍一下ThreadPoolExecutor类,因为以下几种创建线程池的静态方法都是返回ThreadPoolExecutor对象,和我们手动创建ThreadPoolExecutor对象的区别就是我们不需要自己传构造函数的参数。

ThreadPoolExecutor的构造函数共有四个,但最终调用的都是同一个:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize => 线程池核心线程的数量,核心线程池大小是保持活动状态线程的最小值。除非设置 allowCoreThreadTimeOut=true ,这种情况下,最小值为0。
    理解:每当有新的任务到线程池时,先判断线程池中当前线程数量是否达到了corePoolSize,若未达到,则新建线程运行此任务,且任务结束后将该线程保留在线程池中,不做销毁处理。如果达到了corePoolSize,则进行下一步处理。
  • maximumPoolSize => 线程池最大数量。
    线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。
    注意:如果使用了无界的任务队列这个参数就没什么效果。
    在线程池中的线程数量超过corePoolSize时,每当有线程的空闲时间超过了keepAliveTime,这个线程就会被终止。直到线程池中线程的数量不大于corePoolSize为止。
  • keepAliveTime => 等待工作的空闲线程的超时时间(以纳秒为单位)。当存在超过 corePoolSize的线程或 allowCoreThreadTimeOut=true 时,线程使用此超时时间设置。否则,他们会永远等待新的工作。
  • unit => 时间单位
  • workQueue => 线程池所使用的缓冲队列

    可以选择以下几个阻塞队列:
    ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。
    SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue。
    PriorityBlockingQueue:一个具有优先级得无限阻塞队列。

  • threadFactory => 线程池创建线程使用的工厂。
    可以通过线程工厂给每个创建出来的线程设置更有意义的名字,Debug和定位问题时非常有帮助。
  • handler => 线程池对拒绝任务的处理策略
    当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。默认拒绝策略是AbortPolicy,表示无法处理新任务时抛出异常。
    java.util.concurrent.RejectedExecutionHandler是J.U.C包中定义的拒绝策略接口,java.util.concurrent.ThreadPoolExecutor中定义了4个实现RejectedExecutionHandler接口的内部类,即有4种拒绝策略。分别是:
    java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy
    java.util.concurrent.ThreadPoolExecutor.AbortPolicy
    java.util.concurrent.ThreadPoolExecutor.DiscardPolicy
    java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy

allowCoreThreadTimeOut =>

如果为 false(默认值),则核心线程即使在空闲时也保持活动状态。如果为 true,则核心线程使用 keepAliveTime 来超时等待工作。

线程池执行任务逻辑和线程池参数的关系

执行逻辑说明:

  • 判断核心线程数是否已满,核心线程数大小和corePoolSize参数有关,未满则创建线程执行任务

  • 若核心线程池已满,判断队列是否满,队列是否满和workQueue参数有关,若未满则加入队列中

  • 若队列已满,判断线程池是否已满,线程池是否已满和maximumPoolSize参数有关,若未满创建线程执行任务

  • 若线程池已满,则采用拒绝策略处理无法执执行的任务,拒绝策略和handler参数有关

ThreadPoolExecutor线程池的4种拒绝策略

java.util.concurrent.ThreadPoolExecutor.AbortPolicy:直接抛出异常,默认的拒绝策略。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    throw new RejectedExecutionException("Task " + r.toString() +
                                         " rejected from " +
                                         e.toString());
}

java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy:在线程池没有关闭的情况下,让提交任务的线程自己来执行。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }

 java.util.concurrent.ThreadPoolExecutor.DiscardPolicy:什么都不做,直接丢弃。

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }

java.util.concurrent.ThreadPoolExecutor.DiscardOldestPolicy:在线程池没有关闭的情况下,将队列中最早的任务丢弃,然后将该任务放入队列中

public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }

Executors#newCachedThreadPool

CachedThreadPool是一个根据需创建新线程的线程池

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}

corePoolSize => 0,线程池核心线程的数量为0
maximumPoolSize => Integer.MAX_VALUE,可以认为最大线程数是无限的
keepAliveTime => 60L
unit => 秒
workQueue => SynchronousQueue

当一个任务提交时,corePoolSize为0不创建核心线程,SynchronousQueue是一个不存储元素的队列,可以理解为队里永远是满的,因此最终会创建非核心线程来执行任务。
对于非核心线程空闲60s时将被回收。因为Integer.MAX_VALUE非常大,可以认为是可以无限创建线程的,在资源有限的情况下容易引起OOM异常。

Executors#newSingleThreadExecutor

SingleThreadExecutor是单线程线程池,只有一个核心线程

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

corePoolSize => 1,线程池核心线程的数量为1
maximumPoolSize => 1,只可以创建一个非核心线程
keepAliveTime => 0L
unit => 毫秒
workQueue => LinkedBlockingQueue

当一个任务提交时,首先会创建一个核心线程来执行任务,如果超过核心线程的数量,将会放入队列中,因为LinkedBlockingQueue是长度为Integer.MAX_VALUE的队列,可以认为是无界队列,因此往队列中可以插入无限多的任务,在资源有限的时候容易引起OOM异常,同时因为无界队列,maximumPoolSize和keepAliveTime参数将无效,压根就不会创建非核心线程。

Executors#newFixedThreadPool

FixedThreadPool是固定核心线程的线程池,固定核心线程数由用户传入。

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}

corePoolSize => nThreads,线程池核心线程的数量为用户指定
maximumPoolSize => nThreads,非核心线程的数量为用户指定
keepAliveTime => 0L
unit => 秒
workQueue => LinkedBlockingQueue

它和SingleThreadExecutor类似,唯一的区别就是核心线程数不同,并且由于使用的是LinkedBlockingQueue,在资源有限的时候容易引起OOM异常。

总结

  • FixedThreadPool和SingleThreadExecutor => 允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而引起OOM异常

  • CachedThreadPool => 允许创建的线程数为Integer.MAX_VALUE,可能会创建大量的线程,从而引起OOM异常

由于以上三种自动创建线程的方式都存在OOM的缺陷,因此提最好的做法是结合实际情况,手动创建ThreadPoolExecutor对象,详见为什么阿里巴巴要禁用Executors创建线程池

提交任务

一旦创建了一个线程池,就可以往池中通过不同的方法提交执行任务。可提交 Runnable(接口) 或者 Callable(接口) 到线程池中,ExecutorService 的 submit 方法返回一个表示任务状态的 Future 实例。

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

Callable和Runnable区别

Callable源码实现

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Runable源码实现 

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Callable是类似于Runnable的接口,实现Callable接口的类和实现Runnable的类都是可被其它线程执行的任务。
Callable和Runnable有几点不同:
    (1)Callable规定的方法是call(),而Runnable规定的方法是run().
    (2)Callable的任务执行后可返回值,而Runnable的任务是不能返回值的。
    (3)call()方法可抛出异常,而run()方法是不能抛出异常的。
    (4)运行Callable任务可拿到一个Future对象,可了解任务执行情况,可取消任务的执行,还可获取任务执行的结果。

Future

可以通过这个 Future 来判断任务是否执行成功,通过 Future 的 get 方法来获取返回值,调用 get 方法会使父线程阻塞住,必须等到子线程结束后才会得到返回值,调用 get(long timeout, TimeUnit unit) 方法使程序最多阻塞 unit 指定的时间,如果在指定的时间后 Callable 任务依然没有返回值,将会抛出 TimeoutException 异常;
如果通过submit(Runnable task)方法提交一个 Runnable,那么任务完成后
Future 对象将返回 null,如果通过submit(Runnable task, T result)方法提交一个Runnable,那么任务完成后 Future 对象将返回 T。

V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;

例如,编写下面的 Callable
Callable接口使用泛型去定义他的返回类型,
call()方法返回的类型就是传递进来的泛型的类型

private final class StringTask extends Callable<String>{
    public String call(){
        return "Run";
    }
}

如果你想使用4个线程来执行这个任务10次,那么代码如下:

ExecutorService pool = Executors.newFixedThreadPool(4);
for(int i = 0; i < 10; i++){
    pool.submit(new StringTask());
}

用完一个线程池后,应该调用该线程池的 shutdown() 方法,该方法将启动线程池的关闭序列,调用shutdown()方法后的线程池不在接受新任务,但会将以前所有已提交的任务执行完成,当线程池中的所有任务都执行完成之后,池中的所有线程都会死亡。

pool.shutdown();

如果不这么做,JVM 并不会去关闭这些线程;另外可以使用 shutdownNow() 方法来强制关闭线程池,那么执行中的线程也会被中断,所有尚未被执行的任务也将不会再执行。

shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务

我们可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池,但是它们的实现原理不同,shutdown的原理是只是将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。shutdownNow 的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。shutdownNow 会首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。

只要调用了这两个关闭方法的其中一个,isShutdown 方法就会返回 true 。当所有的任务都已关闭后才表示线程池关闭成功,这时调用 isTerminaed 方法会返回 true 。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用 shutdown 来关闭线程池,如果任务不一定要执行完,则可以调用 shutdownNow 

但这个例子中,你无法获取任务的执行状态,因此我们需要借助 Future 接口

ExecutorService pool = Executors.newFixedThreadPool(4); 
List<Future<String>> futures = new ArrayList<Future<String>>(10); 
for(int i = 0; i < 10; i++){ 
   futures.add(pool.submit(new StringTask())); 
} 

for(Future<String> future : futures){ 
   String result = future.get(); 
} 
pool.shutdown(); 

不过这段代码稍微有点复杂,而且有不足的地方。如果第一个任务耗费非常长的时间来执行,然后其他的任务都早于它结束,那么当前线程就无法在第一个任务结束之前获得其他线程的执行结果,但是别着急,Java 为你提供了解决方案——CompletionService(接口)

一个 CompletionService 就是一个服务,用以简化等待任务的执行结果,唯一的实现类是 ExecutorCompletionService,该类基于 ExecutorService,因此我们可试试下面的代码:

ExecutorService threadPool = Executors.newFixedThreadPool(4); 
CompletionService<String> pool = new ExecutorCompletionService<String>(threadPool); 
for(int i = 0; i < 10; i++){ 
   pool.submit(new StringTask()); 
} 
for(int i = 0; i < 10; i++){ 
   String result = pool.take().get(); 
} 
threadPool.shutdown(); 

通过这段代码,我们可以根据执行结束的顺序获取对应的结果,而无需维护一个 Future 对象的集合。

通过 Java 为我们提供的各种工具,可以方便的进行多任务的编程,通过使用 ExecutorsExecutorService 以及 CompletionService 等工具类,我们可以创建复杂的并行任务执行算法,而且可以轻松改变线程数。

可以使用JDK自带的监控工具来监控我们创建的线程数量,运行一个不终止的线程,创建指定量的线程,来观察:
工具目录:JDK_HOME\bin\jconsole.exe

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值