Java 定义了 Executor 接口,并在该接口中定义了 execute() 用于执行一个线程任务,然后通过 ExecutorService 实现 Executor 接口并执行具体的线程操作。
ExecutorService 接口有多个实现类可用于创建不同的线程,5种常用的线程池如下:
名称 | 说明 |
---|---|
newCachedThreadPool | 可缓存的线程池 |
newFixedThreadPool | 固定大小的线程池 |
newScheduledThreadPool | 可做任务调用的线程池 |
newSingleThreadExecutor | 单个线程的线程池 |
newWorkStealingPool | 使用 ForkJoinPool 实现的线程池 |
newCachedThreadPool 用于创建一个缓存线程池。之所以叫缓存线程池,是因为它在创建新线程时如果有可重用的线程,则重用它们,否则重新创建一个新的线程并将其添加到线程池中。对于执行时间很短的任务而言,newCachedThreadPool 能很大程度地重用线程进而提高系统的性能。
若线程池中某线程的 keepAliveTime 超过默认的 60s,则该线程会被终止并从缓存中移除,因此在没有线程任务运行时,newCachedThreadPool 将不会占用系统的线程资源。
在创建线程时需要执行申请 CPU 和内存、记录线程状态、控制阻塞等多项操作,复杂且耗时。因此,在有执行时间很短的大量任务需要执行的情况下,newCachedThreadPool 能够很好地复用运行中的线程(任务已完成但未关闭的线程)资源来提高系统的运行效率。
具体的创建方式如下:
ExecutorService executorService = Executors.newCachedThreadPool();
newFixedThreadPool 用于创建一个固定线程数量的线程池,并将线程资源存放在队列中循环使用。在 newFixedThreadPool 中,如果处于活动状态的线程数量大于或等于核心线程池的数量,则提交新的任务将在阻塞队列中排队,直到有可用的线程资源。
具体的创建方式如下:
ExecutorService executorService1 = Executors.newFixedThreadPool(5);
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);
}
newSingleThreadExecutor 用于保证在线程池中有且只有一个可用的线程,在该线程停止或发生异常时,newSingleThreadExecutor 会启动一个新的线程来代替该线程继续执行任务。
具体创建方式如下:
ExecutorService executorService = Executors.newSingleThreadExecutor();
newWorkStealingPool 用于创建持有足够线程的线程池来达到快速运算的目的,在内部通过使用多个队列来减少各个线程调度产生的竞争。
这里所说的有足够的线程,指 JDK 根据当前线程的运行需求向操作系统申请足够的线程,以保障线程的快速执行,并最大限度地使用系统资源,提高计算机的效率,省去用户根据 CPU 资源估算并行度的过程。
当然,如果开发者想自己定义线程的并发数,则也可以将其作为参数传入。
相关面试题:
在 JVM 源码中将线程的生命周期分为新建(New)、可运行(Runnable)、阻塞(Blocked)、等待(Waiting)、超时等待(Timed_Waiting)和终止(Terminated)这 6 中状态。
在系统运行过程中不断有新的线程被创建,老的线程在执行完毕后被清理,线程在排队获取共享资源或锁时将被阻塞,因此运行中的线程会在可运行、阻塞、等待状态之间来回转换。
具体流程如下:
在 Java 中使用 new 关键字创建一个线程,新建的线程将处于新建状态。在创建线程时主要是为线程分配内存并初始化其成员变量的值。
新建的线程对象在调用 start 方法之后将转为可运行状态。其中的就绪状态指的是 JVM 完成了方法调用栈和程序计数器的创建,等待该线程的调度和运行。
处于就绪状态的线程在竞争到 CPU 的使用权并开始执行 run 方法的线程执行体时,会转为运行中状态,处于运行中状态的线程的主要任务就是执行 run 方法中的逻辑代码。
运行中的线程会主动或被动地放弃 CPU 的使用权并暂停运行,此时该线程将转为阻塞状态,直到再次进入可运行状态,才有机会再次竞争到 CPU 使用权并转换为运行状态。
阻塞状态分为如下三种:
线程在调用了 Object.wait()、Thread.join()、LockSupport.park() 后会进入等待状态。处于等待状态的线程会等待另一个线程执行指定的操作。例如,调用 Object.wait() 的一个线程对象会等待另一个线程调用该对象的 Object.notify() 或 Object.notifyAll() 。调用 Thread.join() 的线程会等待指定的线程退出。
超时等待和等待状态不同的是,处于超时等待状态的线程经过超时时间后会被自动唤醒。线程在调用了 Thread.sleep()、Object.wait()、Thread.join()、LockSupport.parkNanos()、LockSupport.parkUntil() 后会进入超时等待状态。
相关面试题: