二、Java 并发编程(2)

本章概要

  • 5种常用的线程池
    • newCachedThreadPool
    • newFixedThreadPool
    • newScheduledThreadPool
    • newSingleThreadExecutor
    • newWorkStealingPool
  • 线程的生命周期
    • 新建状态:New
    • 就绪状态:Runnable
    • 阻塞状态:Blocked
    • 等待状态:Waiting
    • 超时等待状态:Timed_Waiting

2.3 5种常用的线程池

Java 定义了 Executor 接口,并在该接口中定义了 execute() 用于执行一个线程任务,然后通过 ExecutorService 实现 Executor 接口并执行具体的线程操作。

ExecutorService 接口有多个实现类可用于创建不同的线程,5种常用的线程池如下:

名称 说明
newCachedThreadPool 可缓存的线程池
newFixedThreadPool 固定大小的线程池
newScheduledThreadPool 可做任务调用的线程池
newSingleThreadExecutor 单个线程的线程池
newWorkStealingPool 使用 ForkJoinPool 实现的线程池

2.3.1 newCachedThreadPool

newCachedThreadPool 用于创建一个缓存线程池。之所以叫缓存线程池,是因为它在创建新线程时如果有可重用的线程,则重用它们,否则重新创建一个新的线程并将其添加到线程池中。对于执行时间很短的任务而言,newCachedThreadPool 能很大程度地重用线程进而提高系统的性能。

若线程池中某线程的 keepAliveTime 超过默认的 60s,则该线程会被终止并从缓存中移除,因此在没有线程任务运行时,newCachedThreadPool 将不会占用系统的线程资源。

在创建线程时需要执行申请 CPU 和内存、记录线程状态、控制阻塞等多项操作,复杂且耗时。因此,在有执行时间很短的大量任务需要执行的情况下,newCachedThreadPool 能够很好地复用运行中的线程(任务已完成但未关闭的线程)资源来提高系统的运行效率。

具体的创建方式如下:

ExecutorService executorService = Executors.newCachedThreadPool();

2.3.2 newFixedThreadPool

newFixedThreadPool 用于创建一个固定线程数量的线程池,并将线程资源存放在队列中循环使用。在 newFixedThreadPool 中,如果处于活动状态的线程数量大于或等于核心线程池的数量,则提交新的任务将在阻塞队列中排队,直到有可用的线程资源。

具体的创建方式如下:

ExecutorService executorService1 = Executors.newFixedThreadPool(5);

2.3.3 newScheduledThreadPool

newScheduledThreadPool 用于创建一个可定时调度的线程池,可设置在指定的延迟时间后执行或定期执行某个线程任务:

public static void main(String[] args) {
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
    //1.创建一个延迟 3s 执行的线程
    scheduledExecutorService.schedule(new Runnable() {
        @Override
        public void run() {
            System.out.println("延迟 3s 执行");
        }
    },3, TimeUnit.SECONDS);
    //2.创建一个延迟 1s 执行且每 3s 执行一次的线程
    scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            System.out.println("延迟 1s 执行,每 3s 执行一次");
        }
    },1,3, TimeUnit.SECONDS);
}

2.3.4 newSingleThreadExecutor

newSingleThreadExecutor 用于保证在线程池中有且只有一个可用的线程,在该线程停止或发生异常时,newSingleThreadExecutor 会启动一个新的线程来代替该线程继续执行任务。

具体创建方式如下:

ExecutorService executorService = Executors.newSingleThreadExecutor();

2.3.5 newWorkStealingPool

newWorkStealingPool 用于创建持有足够线程的线程池来达到快速运算的目的,在内部通过使用多个队列来减少各个线程调度产生的竞争。

这里所说的有足够的线程,指 JDK 根据当前线程的运行需求向操作系统申请足够的线程,以保障线程的快速执行,并最大限度地使用系统资源,提高计算机的效率,省去用户根据 CPU 资源估算并行度的过程。
当然,如果开发者想自己定义线程的并发数,则也可以将其作为参数传入。

相关面试题:

  • 常用的线程池有哪些?★★★★☆
  • Java 是如何创建一个定时执行的任务的?★★★☆☆
  • 如果 newFixedThreadPool 线程中处于活动状态的线程数量大于或等于核心线程池的数量,那么该怎样处理?★★☆☆☆

2.4 线程的生命周期

在 JVM 源码中将线程的生命周期分为新建(New)、可运行(Runnable)、阻塞(Blocked)、等待(Waiting)、超时等待(Timed_Waiting)和终止(Terminated)这 6 中状态。

在系统运行过程中不断有新的线程被创建,老的线程在执行完毕后被清理,线程在排队获取共享资源或锁时将被阻塞,因此运行中的线程会在可运行、阻塞、等待状态之间来回转换。

具体流程如下:

  1. 调用 new 方法新建一个线程,这时线程处于新建状态。
  2. 调用 start 方法启动一个线程,这时线程处于可运行状态。可运行状态又分为就绪(Ready)和运行中(Running)两种状态。处于就绪状态的线程等待线程获取 CPU 资源,获取之后,线程会调用 run 方法进入运行状态;处于运行中状态的线程在调用 yield 方法或失去处理器资源时,会再次进入就绪状态。
  3. 处于运行中状态的线程在执行 sleep 方法,I/O 阻塞、等待同步锁、等待通知、suspend 方法等后,会挂起并进入阻塞状态。处于阻塞状态的线程由于出现 sleep 时间已到、I/O 方法返回、获得同步锁、收到通知、调用 resume 方法等情况,会再次进入可运行状态中的就绪状态,等待 CPU 时间片的轮询。该线程在获取 CPU 资源后,会再次进入运行中状态。
  4. 线程在调用 Object.wait()、Object.join()、LockSupport.park() 后会进入等待状态。处于等待状态的线程在调用 Object.notify()、Object.notifyAll()、LockSupport.unpark(Thread) 后会再次进入可运行状态。
  5. 处于可运行状态的线程在调用 Thread.sleep(long)、Object.wait(long)、Thread.join(long)、LockSupport.parkNanos()、LockSupport.parkUntil() 后会进入超时等待状态。当处于超时等待的线程出现超时时间到、等待进入 synchronized 方法、等待进入 synchronized 块或者调用 Object.notify()、Object.notifyAll()、LockSupport.unpark(Thread) 后会再次进入可运行状态。
  6. 处于可运行状态的线程,在调用 run 方法或 call 方法正常执行完成、调用 stop 方法停止线程或者程序执行错误导致异常退出时,会进入终止状态。

2.4.1 新建状态:New

在 Java 中使用 new 关键字创建一个线程,新建的线程将处于新建状态。在创建线程时主要是为线程分配内存并初始化其成员变量的值。

2.4.2 就绪状态:Runnable

新建的线程对象在调用 start 方法之后将转为可运行状态。其中的就绪状态指的是 JVM 完成了方法调用栈和程序计数器的创建,等待该线程的调度和运行。

处于就绪状态的线程在竞争到 CPU 的使用权并开始执行 run 方法的线程执行体时,会转为运行中状态,处于运行中状态的线程的主要任务就是执行 run 方法中的逻辑代码。

2.4.3 阻塞状态:Blocked

运行中的线程会主动或被动地放弃 CPU 的使用权并暂停运行,此时该线程将转为阻塞状态,直到再次进入可运行状态,才有机会再次竞争到 CPU 使用权并转换为运行状态。

阻塞状态分为如下三种:

  1. 等待阻塞:在处于运行状态的线程调用 wait 方法时,JVM 会把该线程放入队列(Waiting Queue)中,线程转为阻塞状态。
  2. 同步阻塞:在处于运行状态的线程尝试获取正在被其它线程占用的对象同步锁时,JVM 会把该线程放入锁池(Lock Pool)中,此时线程转为阻塞状态。
  3. 其它阻塞:在处于运行状态的线程执行 sleep、join、或者发出 I/O 请求时,JVM 会把该线程转为阻塞状态。直到 sleep 状态超时、join 等待线程终止或超时,或者 I/O 处理完毕,线程才能重新转为可运行状态。

2.4.4 等待状态:Waiting

线程在调用了 Object.wait()、Thread.join()、LockSupport.park() 后会进入等待状态。处于等待状态的线程会等待另一个线程执行指定的操作。例如,调用 Object.wait() 的一个线程对象会等待另一个线程调用该对象的 Object.notify() 或 Object.notifyAll() 。调用 Thread.join() 的线程会等待指定的线程退出。

2.4.5 超时等待状态:Timed_Waiting

超时等待和等待状态不同的是,处于超时等待状态的线程经过超时时间后会被自动唤醒。线程在调用了 Thread.sleep()、Object.wait()、Thread.join()、LockSupport.parkNanos()、LockSupport.parkUntil() 后会进入超时等待状态。

相关面试题:

  • 线程的生命周期包括哪几个阶段?★★★★★★
  • 在哪些情况下会出现线程阻塞?★★★☆☆
  • 线程在就绪状态下会立即执行吗?★★☆☆☆
  • 什么是线程阻塞?★★☆☆☆

你可能感兴趣的:(Offer,5种常用的线程池,线程的生命周期)