Java(八)线程池

为什么要使用线程池?(阿里巴巴开发手册推荐使用线程池)
在执行一个异步任务或并发任务时,往往是通过直接new Thread()方法来创建新的线程,这样做弊端较多,更好的解决方案是合理地利用线程池,线程池的优势很明显,如下:
1.降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
2.提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
3.方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
4.更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。

一、线程池相关的类和接口

  1. Executor 接口
    Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),如下:
public interface Executor {
    void execute(Runnable command);
}

ExecutorService:是一个比Executor使用更广泛的子类接口,其提供了生命周期管理的方法,返回 Future 对象,以及可跟踪一个或多个异步任务执行状况返回Future的方法;可以调用ExecutorService的shutdown()方法来平滑地关闭 ExecutorService,调用该方法后,将导致ExecutorService停止接受任何新的任务且等待已经提交的任务执行完成(已经提交的任务会分两类:一类是已经在执行的,另一类是还没有开始执行的),当所有已经提交的任务执行完毕后将会关闭ExecutorService。因此我们一般用该接口来实现和管理多线程。

AbstractExecutorService抽象类实现的是ExecutorService接口。

ThreadPoolExecutor又是继承自AbstractExecutorService类。

  1. Executors工具类
    Executors工具类是用来创建线程池的工厂类,内部返回的是ThreadPoolExecutor类的对象,简化了创建线程池的过程(推荐使用)。如果自带的几种创建方法无法满足要求的话,需要自己手动构建配置ThreadPoolExecutor对象。

二、ThreadPoolExecutor类
1.ThreadPoolExecutor类的构造函数

 public ThreadPoolExecutor( int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

2.ThreadPoolExecutor的成员变量
(1)corePoolSize:核心池的大小,在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务。

(2)maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多少个线程;
按照JDK文档的描述,设poolSize为当前线程池的线程量,则有如下关系:
(a)如果poolSize<corePoolSize,新增加一个线程处理新的任务。
(b)如果poolSize=corePoolSize,新任务会被放入阻塞队列等待。(优先使用队列,队列满了再看maxmumPoolSize)
(c)如果阻塞队列的容量达到上限,且这时poolSize<maximumPoolSize,新增线程来处理任务。
(d)如果阻塞队列满了,且poolSize=maximumPoolSize,那么线程池已经达到极限,会根据饱和策略RejectedExecutionHandler拒绝新的任务。

(3)keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

(4)unit:参数keepAliveTime的时间单位,取值如:TimeUnit.DAYS、TimeUnit.HOURS等

(5)workQueue:一个阻塞队列,用来存储等待执行的任务,阻塞队列有以下几种选择:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue。线程池的排队策略与workQueue有关。

(6)threadFactory:线程工厂,主要用来创建线程;

(7)handler:表示当拒绝处理任务时的策略,有以下四种取值:
□ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
□ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
□ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
□ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

corePool -> 核心线程池
maximumPool -> 线程池
BlockQueue -> 队列
RejectedExecutionHandler -> 拒绝策略

三、ExecuteService
ExecuteService是一个接口,ThreadPoolExecutor类实现了这个接口,这个接口里面包含了线程池的各种控制操作,具体有如下函数。

public interface ExecutorService extends Executor {

    /**
     * 关闭线程池,已提交的任务继续执行,不接受新的任务提交
     */
    void shutdown();

    /**
     * 关闭线程池,尝试关闭正在执行的任务,并且不再接受新的任务提交
     * 比前面多了一个now,区别在于它可以去停止当前正在执行的任务
     */
    List<Runnable> shutdownNow();

    /**
     * 线程池是否已经关闭了
     */
    boolean isShutdown();

    /**
     * 如果调用了shutdown和shutdownnow方法后,所有任务都结束了,那么返回true
     * 该方法必须在调用了shutdown和shutdownNow方法之后调用才会返回true
     */
    boolean isTerminated();

    /**
     * 请求关闭、发生超时或者当前线程中断,无论哪一个首先发生之后,都将导致阻塞,直到所有任务完成执行。
     * 等待所有任务完成,并设置了超时时间
     * 实际中是:先调用shutdown和shutdownNow,然后再调用该方法等待所有线程真正完成,最后的返回值表示是否超时
     */
    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;

    /**
     * 提交一个Callable任务
     * 返回一个表示任务的 Future。该 Future 的 get 方法在成功完成时将会返回该任务的结果。
     */
    <T> Future<T> submit(Callable<T> task);

   /**
    * 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
    * 该 Future 的 get 方法在成功完成时将会返回给定的结果。
    * 因为Runable没有返回值,所有我们制定第二个参数来作为返回值
    */
    <T> Future<T> submit(Runnable task, T result);

    /**
     * 提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。
     * 该 Future 的 get 方法在成功 完成时将会返回 null。
     */
    Future<?> submit(Runnable task);

    /**
     * 执行所有任务,返回Future类型的一个list
     */
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;

    /**
     * 执行给定的任务,当所有任务完成或超时期满时(无论哪个首先发生),返回保持任务状态和结果的 Future 列表。
     * 给invokeAll设置了一个超时时间
     */
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;

    /**
     * 只要其中的任意一个任务结束了就可以返回,返回是那个任务的结果
     */
    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;

   /**
    * 跟上面方法一样,带超时时间,超过超时时间,则抛出TimeoutException异常
    */
    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;

以下的几个函数都是线程池的函数,所以调用方式是 线程池名.方法()
1.关闭线程池的函数是:
(1)shutdown
(2)shutdownNow 比前面多了一个now,区别在于它可以去停止当前正在执行的任务,返回值是关闭的任务列表。

2.提交任务
(1)submit 返回一个Future对象,Future 提供了get方法可以取得方法执行完的结果。如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常。
(2)execute 没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多。

注意:execute方法的参数是Runnable对象,也就是新建的线程:一个Thread类对象
submit的方法参数可以是Runnable对象,也可以Callable对象。

3.Callable和Runnable

public interface Callable<V> {
    V call() throws Exception;
}
public interface Runnable {
    public abstract void run();
}

(1)Runnable没有返回值;Callable可以返回执行结果,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
(2)Callable接口的call()方法允许抛出异常;Runnable的run()方法异常只能在内部消化,不能往上继续抛。

4.Future和FutureTask
(1)Future是一个接口,FutureTask是RunnableFuture接口的实现类,RunnableFuture接口集成了Runnable和Future接口。一般用FutureTask对象给Future赋值。
(2)Future和Futuretask的get()方法的返回值是线程执行结果,同时这个方法也可以抛出异常。

四、Executor类
ThreadPoolExecutor是表示一个线程池,而Executors则是线程工厂的角色,通过Executors,我们可以得到一个拥有特定功能的线程池。
Executor工具类提供了以下几种静态的构造线程池的方法:

newFixedThreadPool(int nThreads)
//创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中。

newWorkStealingPool()
//创建持有足够线程的线程池来支持给定的并行级别,并通过使用多个队列,减少竞争,
//它需要穿一个并行级别的参数,如果不传,则被设定为默认的CPU数量。

newSingleThreadExecutor()
//该方法返回一个固定数量的线程池  
//该方法的线程始终不变,当有一个任务提交时,若线程池空闲,则立即执行,
//若没有,则会被暂缓在一个任务队列只能怪等待有空闲的线程去执行。

newCachedThreadPool() 
//返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,
//若有空闲的线程则执行任务,若无任务则不创建线程,并且每一个空闲线程会在60秒后自动回收。

newScheduledThreadPool(int corePoolSize)
//返回一个SchededExecutorService对象,但该线程池可以设置线程的数量,
//支持定时及周期性任务执行。
 
newSingleThreadScheduledExecutor()
//创建一个单例线程池,定期或延时执行任务。 

1.newFixedThreadPool() 无界线程池,阻塞队列是无界的
该方法创建指定线程数量的线程池,没有限制可存放的线程数量(无界队列),适用于线程任务执行较快的场景。

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

核心线程数和是最大线程数都是传入的参数,存活时间是0,时间单位是毫秒,阻塞队列是无界队列LinkedBlockingQueue。由于队列采用的是无界队列LinkedBlockingQueue,最大线程数maximumPoolSize和keepAliveTime都是无效参数,拒绝策略也将无效。
当线程池中的任务处理不及时的时候,而一边又疯狂的提交任务,将会导致OOM发生(OutOfMemoryError,内存溢出)。

2.newCachedThreadPool 可缓存的线程池,corepoolsize是0但是max是整数最大值
newCachedThreadPool方法创建的线程池会根据需要自动创建新线程。

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

newCachedThreadPool方法也是返回ThreadPoolExecutor对象,核心线程是0,最大线程数是Integer的MAX_VALUE,也就是整数的最大值,存活时间是60,时间单位是秒,SynchronousQueue队列。
该方法将返回一个可根据实际情况调整的线程数量的线程池,线程池的数量不固定,我们可以看见上面的方法中设置的是corePoolSize为0,maximumPoolSize为整数最大值,保活时间为60秒,阻塞队列为SynchronousQueue,故线程池中有空闲线程可以复用的话,则会优先复用空闲线程,如果所有的线程都在工作的话,新的任务提交,直接会创建新的线程处理任务,所有线程处理完任务后,将会返回线程池进行复用。如果同时又大量任务提交,那么将会开启等量的线程,这样也会导致OOM。

3.newSingleThreadExecutor 单线程池,coresize和maxsize都是1

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

可见这个方法,只会创建一个线程的线程池。多余的任务还是会被添加到 LinkedBlockingQueue中,也会有OOM情况的发生。

  1. newScheduledThreadPool()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

该方法也返回一个ScheduledExecutorService对象,不过可以指定核心线程的数量。
ScheduleExecutorService继承自ExecutorService,可以根据时间需要对线程进行调度
schedule:会在给定时间,对任务进行一次调度
scheduleAtFixedRate:周期性调度,任务的调度频率是一定的。它是以上一个任务开始执行时间为起点,之后的period时间,调度下一次任务,即period包含了线程执行的时间的。
scheduleWithFixedDelay:周期性调度,在上一个任务结束后,再经过delay时间进行任务调度。

五、拒绝策略
JVM一共提供了四种拒绝策略
1 AbortPolicy

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

该策略会直接抛出异常,阻止系统正常工作

2 CallerRunsPolicy

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

只要线程未关闭,该策略直接调用者线程中执行将被丢弃的任务,这样的话不会真正抛弃任务,但会影响提交线程的性能。

3 DiscardOldestPolicy

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

丢弃最开始的(即将被执行的)任务,并尝试再次提交任务。

4 DiscardPolicy

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

该策略将直接丢弃无法处理的任务,不予任何处理

六、自定义线程池的创建
在严谨的开发场合,需要自定义线程池,才能准确控制各种情况。
使用ThreadPoolExecutors的构造函数来构造自己需要的线程池。

public class ThreadFactoryDemo {

    public static class MyTask implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+" "+System.currentTimeMillis());
            try {
                Thread.sleep(2000);
                System.out.println(Thread.currentThread().getName()+" "+System.currentTimeMillis());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        MyTask myTask=new MyTask();
        ExecutorService executorService=new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>(), new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t=new Thread(r);
                t.setDaemon(true);
                System.out.println("create"+t.getName());
                return t;
            }
        });

        for (int i = 0; i < 5; i++) {
            executorService.submit(myTask);
        }
        Thread.sleep(1000);
        executorService.shutdown();
    }
}

参数的设定参考如下:
* 需要根据几个值来决定
- tasks :每秒的任务数,假设为500~1000
- taskcost:每个任务花费时间,假设为0.1s
- responsetime:系统允许容忍的最大响应时间,假设为1s
* 做几个计算
(1)corePoolSize 一般设置成平均每秒处理的任务数
* threadcount = 20%taskstaskcout = (500~1000)0.1 = 20%(50~100) 个线程。
20%是根据8020定律得来的,简单理解是80%的情况下,核心的任务数大约占到20%

(2)queueCapacity = (coreSizePool/taskcost)responsetime
* 计算可得 queueCapacity = 80/0.1
1 = 80。意思是队列里的线程最多可以等待1s,所以需要80长度的阻塞队列,这样一秒钟后队列前面80个任务都完成了,所以1s后就能开始执行了。

(3) maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)
* 计算可得 maxPoolSize = (1000-80)/10 = 92
* (最大任务数-队列容量)/每个线程每秒处理能力 = 最大线程数

(4)rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理

(5)keepAliveTime和allowCoreThreadTimeout采用默认通常能满足

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值