Java并发编程-线程池

1.为什么要用线程池

Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。在开发过程中,合理地使用线程池能够带来 3个好处。

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。 如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。线程池技术正是关注如何缩短或调整 T1,T3 时间的技术,从而提高服务器程序性能的。它把 T1,T3 分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这样在服务器程序处理客户请求时,不会有 T1,T3 的开销了。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

假设一个服务器一天要处理 50000 个请求,并且每个请求需要一个单独的线程完成。在线程池中,线程数一般是固定的,所以产生线程总数不会超过线程池中线程的数目,而如果服务器不利用线程池来处理这些请求则线程总数为 50000。一般线程池大小是远小于 50000。所以利用线程池的服务器程序不会为了创建50000 而在处理请求时浪费时间,从而提高效率。

2.ThreadPoolExecutor 的类关系

Java中创建线程池很简单,可以直接使用ThreadPoolExecutor的构造方法。
Java并发编程-线程池_第1张图片
Executor 是一个接口,它是 Executor 框架的基础,它将任务的提交与任务的执行分离开来。
ExecutorService 接口继承了 Executor,在其上做了一些 shutdown()、submit()的扩展,可以说是真正的线程池接口;
AbstractExecutorService 抽象类实现了 ExecutorService 接口中的大部分方法;
ThreadPoolExecutor 是线程池的核心实现类,用来执行被提交的任务。
ScheduledExecutorService 接口继承了 ExecutorService 接口,提供了带"周期执行"功能 ExecutorService
ScheduledThreadPoolExecutor 是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。ScheduledThreadPoolExecutor 比 Timer 更灵活,功能更强大。

ThreadPoolExecutor的构造方法

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

corePoolSize
线程池中的核心线程数,当提交一个任务时,线程池创建一个新线程执行任务,直到当前线程数等于 corePoolSize;
如果当前线程数为 corePoolSize,继续提交的任务被保存到阻塞队列中,等待被执行;如果执行了线程池的 prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize
线程池中允许的最大线程数。如果当前阻塞队列满了,且继续提交任务,则创建新的线程执行任务,前提是当前线程数小于 maximumPoolSize
keepAliveTime
线程空闲时的存活时间,即当线程没有任务执行时,继续存活的时间。默认情况下,该参数只在线程数大于 corePoolSize 时才有用
unit
keepAliveTime 的时间单位
workQueue
workQueue 必须是 BlockingQueue 阻塞队列。当线程池中的线程数超过它的corePoolSize 的时候,线程会进入阻塞队列进行阻塞等待。通过 workQueue,线程池实现了阻塞功能。
用于保存等待执行的任务的阻塞队列,一般来说,我们应该尽量使用有界队列,因为使用无界队列作为工作队列会对线程池带来如下影响。

  1. 当线程池中的线程数达到 corePoolSize 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 corePoolSize。
  2. 由于 1,使用无界队列时 maximumPoolSize 将是一个无效参数。
  3. 由于1和 2,使用无界队列时 keepAliveTime 将是一个无效参数。
  4. 更重要的,使用无界 queue 可能会耗尽系统资源,有界队列则有助于防止资源耗尽,同时即使使用有界队列,也要尽量控制队列的大小在一个合适的范围。
    所 以 我 们 一 般 会 使 用 , ArrayBlockingQueueLinkedBlockingQueue
    SynchronousQueuePriorityBlockingQueue

threadFactory
创建线程的工厂,通过自定义的线程工厂可以给每个新建的线程设置一个具有识别度的线程名,当然还可以更加自由的对线程做更多的设置,比如设置所有的线程为守护线程。
Executors 静态工厂里默认的 threadFactory,线程的命名规则是“pool-数字-thread-数字”。
RejectedExecutionHandler
线程池的饱和策略,当阻塞队列满了,且没有空闲的工作线程,如果继续提交任务,必须采取一种策略处理该任务,线程池提供了 4 种策略:

  1. AbortPolicy:直接抛出异常,默认策略;
  2. CallerRunsPolicy:用调用者所在的线程来执行任务;
  3. DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
  4. DiscardPolicy:直接丢弃任务;

当然也可以根据应用场景实现 RejectedExecutionHandler 接口,自定义饱和策略,如记录日志或持久化存储不能处理的任务。

3.线程池的工作机制

  1. 如果当前运行的线程少于 corePoolSize,则创建新线程来执行任务(注意, 执行这一步骤需要获取全局锁)。
  2. 如果运行的线程等于或多于 corePoolSize,则将任务加入 BlockingQueue。
  3. 如果无法将任务加入 BlockingQueue(队列已满),则创建新的线程来处 理任务。
  4. 如果创建新线程将使当前运行的线程超出 maximumPoolSize,任务将被 拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。

Java并发编程-线程池_第2张图片

4.Java预定义线程池

Executors工具类提供了以下几种预定义线程池

FixedThreadPool

创建使用固定线程数的FixedThreadPool的API。适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。FixedThreadPool的corePoolSize和maximumPoolSize都被设置为创建FixedThreadPool时指定的参数nThreads。
当线程池中的线程数大于 corePoolSize 时,keepAliveTime 为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止。这里把 keepAliveTime 设置为 0L,意味着多余的空闲线程会被立即终止。
FixedThreadPool 使用有界队列 LinkedBlockingQueue 作为线程池的工作队列,队列的容量为Integer.MAX_VALUE。

  • 调用方式
Executors.newFixedThreadPool(10);
  • 内部实现
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
}

SingleThreadExecutor

创建使用单个线程的 SingleThread-Executor 的 API,适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。corePoolSize 和 maximumPoolSize 被设置为 1。其他参数与 FixedThreadPool相同。SingleThreadExecutor 使用有界队列 LinkedBlockingQueue 作为线程池的工作队列,队列的容量为 Integer.MAX_VALUE。

  • 调用方式
Executors.newSingleThreadExecutor();
  • 内部实现
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
}

CachedThreadPool

创建一个会根据需要创建新线程的 CachedThreadPool 的 API。大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。corePoolSize 被设置为 0,即 corePool 为空;maximumPoolSize 被设置为Integer.MAX_VALUE。这里把 keepAliveTime 设置为 60L,意味着 CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。FixedThreadPool 和 SingleThreadExecutor 使用有界队列 LinkedBlockingQueue作为线程池的工作队列。CachedThreadPool 使用没有容量的 SynchronousQueue作为线程池的工作队列,但 CachedThreadPool 的 maximumPool 是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool 会不断创建新线程。极端情况下,CachedThreadPool 会因为创建过多线程而耗尽 CPU 和内存资源。

  • 调用方式
Executors.newCachedThreadPool();
  • 内部实现
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}

WorkStealingPool

利用所有运行的处理器数目来创建一个工作窃取的线程池,使用 Fork/Join 实现。

  • 调用方式
Executors.newWorkStealingPool(30);
  • 内部实现
public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
}

ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor可以用来在给定延时后执行异步任务或者周期性执行任务,相对于任务调度的Timer来说,其功能更加强大,Timer只能使用一个后台线程执行任务,而ScheduledThreadPoolExecutor则可以通过构造函数来指定后台线程的个数。

  • 调用方式
Executors.newScheduledThreadPool(10)
  • 内部实现
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
}

5.线程池使用示例

将线程放入线程池的两种方法

  1. ExecutorService 类中的 3个submit()方法
    Future submit(Runnable task)
    Future submit(Runnable task, T result)
    Future submit(Runnable task, T result)

  2. Executor 接口中的 void execute(Runnable command)方法

这两种方法的区别在于
submit()方法,可以提供Future 类型的返回值。
executor()方法,无返回值。

线程池的使用示例

public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                System.out.println("我是线程:" + Thread.currentThread().getName() + ",我要开始工作了,当前时间:"+ LocalDateTime.now());
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("我是线程:" + Thread.currentThread().getName() + ",-------我完成工作了---------,当前时间:"+ LocalDateTime.now());
            });
        }
        executorService.shutdown();
}

你可能感兴趣的:(Java,java,多线程,并发编程)