Java工程师的进阶之路 并发篇(一)

849 阅读9分钟

白菜Java自习室 涵盖核心知识

Java工程师的进阶之路 并发篇(一)
Java工程师的进阶之路 并发篇(二)
Java工程师的进阶之路 并发篇(三)
Java工程师的进阶之路 并发篇(四)
Java工程师的进阶之路 并发篇(五)

Java并发-线程池

对象复用思想在编程中有很多应用,不论是线程池还是连接池都是一种对象复用的思想。Java 中创建和销毁一个线程是比较昂贵的操作,需要系统调用。频繁创建和销毁线程会影响系统性能,于是线程池应运而生。

1. 线程池的优势

  1. 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  2. 提高系统响应速度,当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
  3. 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生 OOM,并且会造成 CPU 过度切换。CPU 切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场)。
  4. 提供更强大的功能,延时定时线程池。

2. 线程池的主要参数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)
  1. corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于 corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于 corePoolSize 时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
  2. maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于 maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
  3. keepAliveTime(线程存活保持时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
  4. workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。
  5. threadFactory(线程工厂):用于创建新线程。threadFactory 创建的线程也是采用 new Thread() 方式,threadFactory 创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
  6. handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。

3. 线程池的执行流程

  1. 判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。已满则。
  2. 判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
  3. 判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。

4. 线程池的阻塞队列

  1. 线程若是无限制的创建,可能会导致内存占用过多而产生 OOM,并且会造成 CPU 过度切换。
  2. 线程池创建线程需要获取 mainlock 这个全局锁,影响并发效率,阻塞队列可以很好的缓冲。
  3. 阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入 wait 状态,释放 CPU 资源。

5. 线程池的饱和策略

  1. AbortPolicy:抛出 RejectedExecutionException 来拒绝新任务的处理。
  2. CallerRunsPolicy:调用执行自己的线程运行任务,也就是直接在调用 execute() 方法的线程中运行run() 被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果您的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。
  3. DiscardPolicy: 不处理新任务,直接丢弃掉。
  4. DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

6. 线程池的配置选择

  1. CPU密集型任务:尽量使用较小的线程池,一般为 CPU 核心数 +1。 因为 CPU 密集型任务使得 CPU 使用率很高,若开过多的线程数,会造成 CPU 过度切换。
  2. IO密集型任务:可以使用稍大的线程池,一般为 2*CPU 核心数。 IO 密集型任务 CPU 使用率并不高,因此可以让 CPU 在等待 IO 的时候有其他线程去处理别的任务,充分利用 CPU 时间。
  3. 混合型任务:可以将任务分成 IO 密集型和 CPU 密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。

7. Java提供的线程池

  1. newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成 CPU 过度切换)
  2. newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
  3. newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
  4. newScheduledThreadPool:适用于执行延时或者周期性任务。

8. execute()和submit()方法

  1. execute(),执行一个任务,没有返回值。
  2. submit(),提交一个线程任务,有返回值。
  • submit(Callable task):能获取到它的返回值,通过 future.get() 获取(阻塞直到任务执行完)。一般使用 FutureTask+Callable 配合使用(IntentService中有体现)。
  • submit(Runnable task, T result):能通过传入的载体result间接获得线程的返回值。submit(Runnable task) 则是没有返回值的,就算获取它的返回值也是 null。
  • Future.get():方法会使取结果的线程进入阻塞状态,知道线程执行完成之后,唤醒取结果的线程,然后返回结果。

Java并发-Executor

Java 的线程既是工作单元,也是执行机制。从 JDK 1.5 开始,把工作单元与执行机制分离开来。工作单元包括 Runnable 和 Callable,而执行机制由 Executor 框架提供。

在上层,Java 多线程程序通常把应用分解为若干个任务,然后使用用户级的调度器(Executor 框架)将这些任务映射为固定数量的线程;在底层,操作系统内核将这些线程映射到硬件处理器上。

1. Executor 框架的结构

  • 任务:包括被执行任务需要实现的接口:Runnable 接口或 Callable 接口。
  • 任务的执行:包括任务执行机制的核心接口 Executor,以及继承自 Executor 的 ExecutorService 接口。Executor 框架有两个关键类实现了 ExecutorService 接口(ThreadPoolExecutor 和 ScheduledThreadPoolExecutor)。
  • 异步计算的结果:包括接口 Future 和实现 Future 接口的 FutureTask 类。

2. Executor 类和接口示意图

Executor类 和接口示意图.png

  1. Executor 是一个接口,它是 Executor 框架的基础,它将任务的提交与任务的执行分离开来。
  2. ThreadPoolExecutor 是线程池的核心实现类,用来执行被提交的任务。
  3. ScheduledThreadPoolExecutor 是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。ScheduledThreadPoolExecutor 比 Timer 更灵活,功能更强大。

3. Executor 框架的使用示意图

Executor框架的使用示意图.png

  1. Future 接口和实现 Future 接口的 FutureTask 类,代表异步计算的结果。
  2. Runnable 接口和 Callable 接口的实现类,都可以被 ThreadPoolExecutor 或ScheduledThreadPoolExecutor 执行。

通过 execute() 方法提交的任务都是没有返回值类型的,通过 submit() 提交的任务的返回值类型是 Future,通过 Future 获取线程执行结果。

4. ThreadPoolExecutor

ThreadPoolExecutor 通常使用工厂类 Executors 来创建。

Executors 可以创建3种类型的 ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool 和 CachedThreadPool。

  • SingleThreadExecutor: SingleThreadExecutor 适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。
public static ExecutorService newSingleThreadExecutor()

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)
  • FixedThreadPool: FixedThreadPool 适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。
public static ExecutorService newFixedThreadPool(int nThreads)

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactoty)
  • CachedThreadPool: CachedThreadPool 是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。
public static ExecutorService newCachedThreadPool()

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)

5. ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor:通常用来创建定时线程任务的线程池,例如定时轮询数据库中的表的数据。

ScheduledThreadPoolExecutor通常使用工厂类Executors来创建。Executors可以创建2种类型的ScheduledThreadPoolExecutor,如下:

  • ScheduledThreadPoolExecutor: ScheduledThreadPoolExecutor 适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的应用场景。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
  • SingleThreadScheduledExecutor: ScheduledThreadPoolExecutor 适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。
public static ScheduledExecutorService newSingleThreadScheduledExecutor()

public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory)

6. Future 接口

Future 接口和实现 Future 接口的 FutureTask 类用来表示异步计算的结果。当我们把 Runnable 接口或Callable 接口的实现类提交(submit)给 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 时,ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 会向我们返回一个 FutureTask 对象。

Future submit(Callable task)

Future submit(Runnable task, T result)

Future<> submit(Runnable task)

7. Runnable 接口和 Callable 接口

Runnable 接口和 Callable 接口的实现类,都可以被 ThreadPoolExecutor 或ScheduledThreadPoolExecutor 执行。它们之间的区别是 Runnable 不会返回结果,而 Callable 可以返回结果。除了可以自己创建实现 Callable 接口的对象外,还可以使用工厂类 Executors 来把一个 Runnable 包装成一个 Callable。

public static Callable<Object> callable(Runnable task)

public static <T> Callable<T> callable(Runnable task, T result)

提交给 ThreadPoolExecutor 或 ScheduledThreadPoolExecutor 执行时,submit() 会向我们返回一个 FutureTask 对象。我们可以执行 FutureTask.get() 方法来等待任务执行完成。当任务成功完成后FutureTask.get() 将返回该任务的结果。例如,如果提交的是 callable(Runnable task),FutureTask.get() 方法将返回 null;如果提交的是 callable(Runnable task, T result),FutureTask.get() 方法将返回 result 对象

Java工程师的进阶之路 并发篇(一)
Java工程师的进阶之路 并发篇(二)
Java工程师的进阶之路 并发篇(三)
Java工程师的进阶之路 并发篇(四)
Java工程师的进阶之路 并发篇(五)