并发编程:线程池的使用


一、使用线程池的好处

1. 降低资源消耗

通过重复利用已创建的线程降低线程创建和销毁造成的消耗

2. 提高响应速度

一个任务完成的时间T=创建线程的时间T1+线程执行任务的时间(包括线程同步的时间)T2+线程销毁的时间T3。线程池技术关注于缩短与调整T1T3的时间,提高程序的性能

3. 提高线程的可管理性

线程是稀有资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

4. 防止服务器过载,形成内存溢出,或者cpu耗尽

二、JDK中线程池相关接口与类的关系

1. Executor

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

是Executor框架的基础,将任务的提交与任务的执行分离开来

2. ExecutorService

public interface ExecutorService extends Executor {

    void shutdown();

    List<Runnable> shutdownNow();

    boolean isShutdown();

    boolean isTerminated();

    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;

    <T> Future<T> submit(Callable<T> task);

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

    Future<?> submit(Runnable task);

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;

    <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;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

继承Executor接口,增加了一些shutdown(),submit()方法,对Executor做了一些扩展

3. AbstractExecutorService

是一个实现了Executor接口大部分方法的抽象类

4. ThreadPoolExecutor

线程池的核心实现类,用来执行被提交的任务

5. ScheduledExecutorServicce

继承ExecutorService,提供了带周期性执行的功能

6. ScheduledThreadPoolExecutor

ScheduledExecutorService的实现类,提供了延迟运行和周期性运行的功能

三、创建线程池各个参数的含义

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

1.corePoolSize

线程池的核心线程数

提交一个任务时,如果当前线程数小于corePoolSize,线程池会创建一个新线程执行任务,如果当前线程等于corePoolSize,继续提交任务会将任务保存在阻塞队列中,等待被执行,如果执行了prestartAllCoreThreads()方法,线程池将会提前启动所有的核心线程

2.maxinumPoolSize

线程池允许的最大线程数

如果当前阻塞队列满了,继续提交任务,如果同时当前线程数小于maxinumPoolSIze时会创建新的线程执行任务

3.keepAliveTime

线程空闲时的存活时间,默认情况下该参数只在线程数大于corePoolSize时才生效

4.TimeUnit

keepAliveTime的时间单位

5.workQueue

保存等待任务的阻塞队列

一般来说,我们会使用有界队列,例如:ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue,PriorityBlockingQueue

5.1. 线程池中使用无界队列会出现的问题

1.使用无界队列maxinumPoolSize将是一个无效参数
2.maxinumPoolSize无效,线程数将永远不会超过corePoolSize
3.由于第2点,KeepAliveTime参数也无效
4.使用无界队列可能会耗尽系统资源,即使使用有界队列也要合理设置队列的大小

6.threadFactory

创建线程的工厂,可以更灵活的设置线程

7.RejectedExecutionHandler

线程池的拒绝策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,就会采取一种策略来处理该任务。线程池提供了四种策略,一般我们会实现该接口,自定义拒绝策略

7.1. AbortPolicy

直接抛出异常(默认策略)

7.2. CallerRunsPolicy

用调用者所在的线程执行该任务

7.3. DiscardOldestPolicy

丢弃阻塞队列最靠前的任务,并执行当前任务

7.4. DiscardPolicy

直接丢弃该任务

四、线程池的工作机制

1.提交任务时,当前线程数小于corePoolSize,新启线程执行该任务
2.当前线程数大于等于corePoolSize,将任务保存到BlockingQueue
3.如果阻塞队列已满,且运行的线程线程数不超过maxinumPoolSize,创建新的线程执行任务
4.如果当前线程数大于等于maxinumPoolSize,则调用拒绝策略处理该任务

五、线程池的扩展

ThreadPoolExecutor为我们提供了类似AOP的两个方法供我们自己实现beforeExecute(),afterExecute(),每个任务执行前后都会分别执行这两个方法,在调用shutdown()后会调用terminated()

@Override
protected void beforeExecute(Thread t, Runnable r) {
    System.out.println("beforeExecute " + t.getName());
}

@Override
protected void afterExecute(Runnable r, Throwable t) {
    System.out.println("beforeExecute");
}

@Override
protected void terminated() {
    System.out.println("terminated");
}

六、提交任务

1. execute()

用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功

2. submit()

用于提交需要返回值的任务,线程池会返回一个future类型的对象,通过future对象判断任务是否执行成功,并且通过future对象的get()获取返回值,get()方法会阻塞当前线程直到任务完成,get(long timeout,TimeUnit unit)方法会阻塞当前线程一段时间后立即返回,任务不一定在这个时间段内执行完成

七、关闭线程池

可以通过调用shutdown()和shutdownNow()来关闭线程池,原理是遍历线程池中的工作线程,依次调用interrupt()来中断线程,所以无法响应中断的任务无法终止
两者之间的区别:
shutdown:中断空闲的线程
shutdownNow:中断所有的线程

八、合理配置线程池线程数的大小

CPU核心数可以用Runtime.getRuntime().availableProcessors()获取

1. 高并发,任务执行时间短的业务

线程池线程数可以设置为cpu核心数+1,用来减少线程的上下文切换

2. 并发度不高,任务执行时间长

2.1 任务属于CPU密集型任务

线程池线程数可以设置为cpu核心数+1,用来减少线程的上下文切换

2.2 任务是IO密集型任务

设置较多线程,通常为:CPU核心数*2

3.并发高,任务执行时间长

这种情况不仅仅是设置线程池可以解决的,首先应该从架构上考虑,先采用一些方案减少服务器压力,把服务器并发度降下来,在参考2的配置,如(缓存,异步机制,集群…)

九、JDK中定义的线程池

1. FixedThreadPool

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

创建一个固定线程数的线程池
通过参数我们可知,corePoolSize和maximumPoolSize都被设置为nThreads,线程池数量固定,keepAliveTime设置为0,空闲的线程会立即被终止,阻塞队列为基于链表的LinkedBlockingQueue,容量为Integer.MAX_VALUE。

2. SingleThreadExecutor

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

创建一个单个线程的线程池,适用于需要顺序执行各个任务,并且任意时间最多只有一个线程运行
corePoolSize和maximumPoolSize设置为1,其他与FixedThreadPool相同

3. CachedThreadPool

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

创建一个根据需求创建新线程的,大小无界的线程池,适用于执行很多短期异步任务的小程序,或者负载较轻的服务器
由参数可知,corePoolSize为0,也就是说核心线程数为空,maximumPoolSize为Integer.MAX_VALUE,空闲线程存活时间为60s。阻塞队列使用SynchronousQueue,这是一个没有容量的阻塞队列
如果任务的提交速度大于线程的处理速度,那么CachedThreadPool就会一直创建新的线程,这样会耗尽系统的资源

4. WorkStealingPool

public static ExecutorService newWorkStealingPool(int parallelism) {
    return new ForkJoinPool
        (parallelism,
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}


public static ExecutorService newWorkStealingPool() {
    return new ForkJoinPool
        (Runtime.getRuntime().availableProcessors(),
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}

通过fork/join创建一个工作窃取的线程池

5. ScheduledThreadPoolExecutor

通过工厂类Executors来创建,可以创建两种类型的ScheduledThreadPoolExecutor

  1. ScheduledThreadPoolExecutor
    适用于需要多个线程执行周期任务
  2. SingleThreadScheduledExecutor
    适用于需要单个线程执行周期任务,同时需要保证顺序的执行各个任务

十、线程池的状态

1. RUNNING

线程池的初始状态
任务数为0

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

该状态可以接收新的任务,对添加的任务进行处理

2. SHUTDOWN

调用shutdown(),线程状态由RUNNING切换为SHUTDOWN
该状态不接收新任务,但能处理已添加的任务

3. STOP

调用shutdownNow(),线程状态由RUNNING切换为STOP
该状态不接受新任务,不处理已添加的任务,并且会中断已添加的任务

4. TIDYING

当线程池在SHUTDOWN状态下,阻塞队列为空且线程池中执行的任务也为空时,线程状态由SHUTDOWN切换为TIDYING。当线程池在STOP状态下时,线程池中执行的任务为空时,线程状态由STOP切换为TIDYING。总的来说,就是当所有的任务终止,ctl为0时,线程池状态切换为TIDYING。
该状态时,线程池会执行钩子方法terminated(),该方法默认为空,用户可以重载该方法进行相应的处理

5. TERMINATED

TIDYING状态执行完terminated()方法后,状态就会切换为TERMINATED
线程池彻底终止就是TERMINATED状态

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值