Java-并发-线程池

0.背景

参考资料:Java线程池实现原理及其在美团业务中的实践

在 Java 早期,每次创建线程时,都要涉及到线程的创建、销毁以及资源管理,这对于系统的性能和资源利用率是一种浪费。

因此,Java 提供了线程池的概念,以提高线程的管理效率和性能。

  • 资源管理优化:传统的线程创建和销毁需要涉及系统资源的分配和释放,而频繁的线程创建和销毁会导致资源的浪费和性能下降。

    线程池通过维护一组可重用的线程来减少线程的创建和销毁次数,从而优化了系统资源的利用。

  • 线程复用:线程池可以维护一定数量的线程池,这些线程可以被重复利用来执行多个任务,而不必每次都创建新线程。

    这样可以避免频繁地创建和销毁线程,提高了系统的性能和效率。

  • 任务调度和管理:线程池可以对任务进行调度和管理,可以根据任务的优先级和类型来调度执行线程,提供了更加灵活和可控的任务执行方式。

  • 并发控制:线程池提供了一种方便的方式来控制并发执行的线程数量,可以限制同时执行的线程数量,从而避免因线程过多导致系统资源耗尽或性能下降的问题。

1.概念

1.1 关键词

记忆方法:核大存单队略厂(这里和大存单的队伍略长)

1.1.1 核心线程数 corePoolSize

  • 含义:指定线程池中保持的活动线程数。除非设置了 allowCoreThreadTimeOuttrue,否则这些线程不会被回收。
  • 作用:核心线程数决定了线程池中保持的最小活动线程数,即使线程处于空闲状态也不会被回收。

1.1.2 最大线程数 maximumPoolSize

  • 含义:指定线程池中允许创建的最大线程数。当任务队列已满且核心线程数已达到最大时,线程池会创建新的线程来执行任务,直到达到最大线程数为止。
  • 作用:最大线程数决定了线程池中允许创建的最大线程数,用于处理任务队列中的任务。

1.1.3 空闲线程存活时间 keepAliveTime

  • 含义:指定空闲线程的存活时间,即当线程池中的线程处于空闲状态且空闲时间超过该值时,这些空闲线程可以被回收。
  • 作用:通过设置线程的存活时间,可以控制线程池中线程的数量,避免因线程过多导致系统资源的浪费。

1.1.4 空闲线程存活时间单位 unit

  • 含义:指定空闲线程存活时间的单位,可以是纳秒、微秒、毫秒、秒、分钟、小时或天等。
  • 作用:单位参数用于指定空闲线程存活时间的时间单位,与 keepAliveTime 参数配合使用。

1.1.5 工作队列 workQueue

  • 含义:任务队列,用于保存等待执行的任务。线程池中的线程会从任务队列中取出任务执行。
  • 作用:工作队列用于存储提交给线程池但尚未执行的任务,当线程池中的线程处于忙碌状态时,新的任务会被放入任务队列等待执行。

1.1.6 拒绝策略 rejectedExecutionHandler

  • 含义:拒绝策略,用于处理当任务无法被线程池执行时的情况。当任务队列已满且无法接受新的任务时,或者线程池已关闭但仍然有任务提交时,会触发拒绝策略。
  • 作用:拒绝策略定义了线程池无法处理任务时的行为,包括抛出异常、丢弃任务、阻塞调用者或者执行任务的线程等待一段时间后再尝试执行任务等。选择合适的拒绝策略能够有效地处理任务提交过载的情况,保护线程池和系统的稳定性。

1.1.7 线程工厂 threadFactory

  • 含义:线程工厂,用于创建新的线程。当线程池需要创建新线程时,会调用线程工厂的 newThread(Runnable r) 方法来创建线程。
  • 作用:线程工厂用于创建线程池中的线程实例,通过自定义线程工厂,可以控制线程的创建过程,如设置线程的名称、优先级、是否为守护线程等。

1.2 线程池的状态

线程池运行的状态,并不是用户显式设置的,而是伴随着线程池的运行,由内部来维护。

运行状态 是否接收任务 是否处理任务 状态描述
RUNNING 能接受新提交的任务,并且也能处理阻塞队列中的任务。
SHUTDOWN × 不再接受新提交的任务,可以继续处理阻塞队列中已保存的任务。
STOP × × 不再接受新任务,也不继续处理队列中的任务,会中断正在处理任务的线程。
TIDYING - - 所有的任务都已终止了,workerCount(有效线程数)为0。
TERMINATED - - 终止状态,在 terminated() 方法(预留)执行完后进入该状态。

状态转换如下:

图3 线程池生命周期

1.3 运行过程

有核心线程,优先用核心线程。核心线程不够用,先往工作队列里放。工作队列都放不下了,再创建非核心线程来执行。实在是不行了,只能进行拒绝。

  • 开始,接收到提交的一个任务。

  • 判断当前工作线程数与核心线程数的关系

    • 工作线程数 < 核心线程数,新建工作线程(核心)并执行。

    • 工作线程数 >= 核心线程数

      检查工作队列情况

      • 工作队列未满,添加任务到工作队列中,等待获取执行。

      • 工作队列已满

        检查最大线程数

        • 未达到最大线程数,新建工作线程(非核心)执行。
        • 已经达到最大线程数,进行任务拒绝。

img

1.4 理解

注意,以下是我的理解,我半桶水,你记得带自己的思考来阅读喔。

1.4.1 核心线程数和最大线程数

忽略核心线程是否可以销毁

核心线程必定在运行,是我们使用的最小成本。而最大线程是一个兜底机制,不可能无休止的来创建线程,必定要有个度。

线程池中的任务,是依赖核心线程和非核心线程来执行的,不是说跟线程创建销毁跟工作队列没关系,而是侧重点不同,工作队列的目的是缓冲。

1.4.2 工作队列存在的意义

工作队列的存在,是为了提供一个缓冲,帮助线程池保持一个最小可运转的线程数量,避免频繁的创建和销毁线程。

好,我们回顾上面的图,核心线程没满的时候,我们可以新建线程来执行,在核心线程内的数量,我们是很好接受的。

那么,当我的核心线程都不够用了,这个时候,暴力的方式当然是直接新建线程来执行。

  • 优点,核心线程在忙碌,我直接搂一个新线程,立马就能执行,效率很高。
  • 缺点,涉及到的频繁的创建线程(高成本)。

有了工作队列后,我们核心线程都在忙碌时,选择先将任务放到队列里等一等,实在再放不下了,再来创建线程执行。

我们还可以极端点,假设我们新建一个线程要2s,执行一个任务只要0.5s。

核心线程已满,这个时候,我们是不是等个0.5s就能空闲出一个核心线程,然后就有得执行了。而不是选择立马新建一个,耗费2s。

1.4.3 ThreadPoolExecutor分析

1.4.3.1 关于参数校验

经常会有面试问,核心线程数能设置为0吗?

注意这是构造方法,是做了校验的。

A.参数不合法异常 IllegalArgumentException
  • 核心线程数 < 0
  • 最大线程数 <= 0
  • 最大线程数 < 核心线程数
  • 线程存活时间小于0
B.空指针异常NullPointerException
  • 工作队列为null
  • 线程工厂为null
  • 拒绝策略为null
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

这个构造函数,最少需要5个核心参数喔。

image-20240509154827473

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
1.4.3.2 核心线程数可以为0吗?

可以

线程池也可以接收任务,线程池会依赖非核心线程来执行任务。

任务首先会被放入工作队列进行缓冲,如果工作队列已满,再根据最大线程数的限制来新建非核心线程来执行任务。

只有在工作队列已满,且线程池中的线程数未达到最大线程数时,才会创建新的线程,如果线程池已达到最大线程数,则不会再创建新线程,根据拒绝策略来处理任务。

然后呢,这个时候也有个问题,由于我们没有核心线程,直接被放到工作队列里,这个队列没满的话,就不会创建线程,相当于在干等。

嗯,就跟我们写的代码一样,让人感到无语。

1.4.3.3 最大线程数可以为0吗?

不可以

首先通过前面可以知道,最大线程数必须比核心线程数大,然后呢,核心线程数不能小于0(参数异常),但是核心线程数可以等于0。

然后呢,还有个校验,是最大线程数是必须大于核心线程数的,所以说呀,这个最大线程数,最小值是1

哦,sorry,最大线程数的判断直接是maximumPoolSize <= 0 抛异常。

我没骗你,你自己看图。

image-20240509155047977

image-20240509155030262

1.4.4 线程池中的ctl属性

在线程池的代码中,经常能看到一个ctl.get()在使用,这个ctl是什么呢?它是其中的一个属性值。

image-20240509195730836

嗯,又是一个高深的设计,看不懂。

暂时理解为这玩意能获取到当前线程池状态和当前工作线程数量

高位表示线程池的运行状态runState,低位表示线程池的工作线程数量workerCount,然后框框下面不是还两个of结尾的方法嘛,就方便的拿到这两个内容了。

2.类说明

2.1 Executor接口

  • 定义了一个单一的方法 execute(Runnable command),用于执行提交的任务。
  • 只关注任务的提交方式,不关心任务的执行细节。

image-20240507223239290

2.2 ExecutorService接口

  • Executor 接口的子接口,提供了更多的方法用于任务管理和线程池控制。
  • 定义了一系列方法来提交任务、关闭线程池、管理任务的执行状态等。

image-20240507223408199

2.3 ThreadPoolExecutor类

image-20240509203241272

  • ExecutorService 接口的一个具体实现类,也是 Executors 类创建线程池的底层实现。
  • 提供了一个灵活的线程池实现,可以通过构造方法来设置各种参数,如核心线程数、最大线程数、任务队列、拒绝策略等。

image-20240507223451061

2.4 Executors类

  • 工具类,提供了一系列静态方法用于创建不同类型的内置线程池。
  • 通过 Executors 类可以快速方便地创建各种预定义类型的线程池,如固定大小线程池、可缓存线程池等

image-20240507223316563

2.4.1 FixedThreadPool

固定线程数量的线程池,该线程池中的线程数量始终不变。

当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。

若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class FixedThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个固定大小的线程池,大小为3
        ExecutorService executorService = Executors.newFixedThreadPool(3);

        // 提交10个任务给线程池
        for (int i = 0; i < 10; i++) {
            int taskNumber = i;
            executorService.submit(() -> {
                System.out.println("Task " + taskNumber + " executed by thread: " + Thread.currentThread().getName());
            });
        }

        // 关闭线程池
        executorService.shutdown();
    }
}

2.4.2 SingleThreadExecutor

只有一个线程的线程池。

若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleThreadExecutorExample {
    public static void main(String[] args) {
        // 创建一个单线程的线程池
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        // 提交10个任务给线程池
        for (int i = 0; i < 10; i++) {
            int taskNumber = i;
            executorService.submit(() -> {
                System.out.println("Task " + taskNumber + " executed by thread: " + Thread.currentThread().getName());
            });
        }

        // 关闭线程池
        executorService.shutdown();
    }
}

2.4.3 CachedThreadPool

可根据实际情况调整线程数量的线程池。

线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。

若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CachedThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个缓存线程池
        ExecutorService executorService = Executors.newCachedThreadPool();

        // 提交10个任务给线程池
        for (int i = 0; i < 10; i++) {
            int taskNumber = i;
            executorService.submit(() -> {
                System.out.println("Task " + taskNumber + " executed by thread: " + Thread.currentThread().getName());
            });
        }

        // 关闭线程池
        executorService.shutdown();
    }
}

2.4.4 ScheduledThreadPool

给定的延迟后运行任务或者定期执行任务的线程池

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ScheduledThreadPoolExample {
    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);

        // 延迟执行任务
        scheduledExecutorService.schedule(() -> {
            System.out.println("Task executed after 2 seconds.");
        }, 2, TimeUnit.SECONDS);

        // 周期性执行任务
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            System.out.println("Task executed every 3 seconds.");
        }, 0, 3, TimeUnit.SECONDS);

        // 关闭线程池
        scheduledExecutorService.shutdown();
    }
}

3.阻塞队列

常见线程池及其使用到的阻塞队列如下

线程池 中文名 核心线程数 阻塞队列类型 工作队列容量 特点 可能的问题
FixedThreadPool 固定大小线程池 固定 LinkedBlockingQueue Integer.MAX_VALUE 只能创建核心线程数的线程,任务队列永远不会被放满 工作队列无界,可能导致OOM。
SingleThreadExecutor 单线程线程池 1 LinkedBlockingQueue Integer.MAX_VALUE 只能创建一个线程,任务队列永远不会被放满 工作队列无界,可能导致OOM。
CachedThreadPool 缓存线程池 动态调整
最大Integer.MAX_VALUE
SynchronousQueue 无容量 最大线程数为Integer.MAX_VALUE,可能会创建大量线程 线程数过多,可能导致OOM。
ScheduledThreadPool 定时线程池 固定 DelayedWorkQueue Integer.MAX_VALUE 队列添加元素满了之后会自动扩容,最多只能创建核心线程数的线程 工作队列每次自动扩容1/2,可能导致OOM。
SingleThreadScheduledExecutor 单线程定时线程池 1 DelayedWorkQueue Integer.MAX_VALUE 队列添加元素满了之后会自动扩容,最多只能创建一个线程 工作队列每次自动扩容1/2,可能导致OOM。

4.拒绝策略

当线程池已经达到了最大线程数限制,且工作队列也已满(无法接受新任务),此时如果继续有新的任务提交给线程池,就会触发拒绝策略。

4.1 默认的拒绝策略

ThreadPoolExecutor中默认的拒绝策略如下

序号 名称 解释 适用场景
1 AbortPolicy(默认) 拒绝并抛出异常 希望立即知道任务被拒绝,并且需要通知调用者处理拒绝的情况时使用。
2 CallerRunsPolicy 使用调用者线程执行 希望调用者线程直接执行任务,一般在不允许失败的、对性能要求不高、并发量较小的场景下使用。
可能会导致主线程阻塞或延迟响应其他请求。
3 DiscardPolicy 直接丢弃任务 对任务执行顺序无要求,可以丢弃一些任务时使用。
4 DiscardOldestPolicy 丢弃最旧的任务 希望尽量保留新提交的任务,并且对于旧任务执行顺序无要求时使用。

image-20240510132616682

4.2 自定义拒绝策略

自定义拒绝策略需实现 RejectedExecutionHandler 接口并覆盖 rejectedExecution 方法。

该方法接收两个参数:被拒绝的任务执行该任务的线程池

import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
    
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 在这里实现自定义的拒绝策略,例如记录日志、尝试重新提交任务等
        System.err.println("任务被拒绝:" + r.toString());
        // 可以选择尝试重新提交任务
        // executor.submit(r);
    }
}

在创建线程池时,将自定义的拒绝策略传递给 ThreadPoolExecutor 的构造函数即可:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,      // 核心线程数
    maxPoolSize,       // 最大线程数
    keepAliveTime,     // 空闲线程存活时间
    TimeUnit.SECONDS,  // 时间单位
    new ArrayBlockingQueue<>(queueCapacity),  // 任务队列
    new CustomRejectedExecutionHandler());    // 自定义拒绝策略

5.线程池关闭

当线程池不再需要使用时,应该显式地关闭线程池,释放线程资源。

5.1 shutdown()平滑关闭

线程池的状态变为 SHUTDOWN。线程池不再接受新任务了,但是队列里的任务得执行完毕。

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(SHUTDOWN);
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
    }

5.2 shutdownNow()暴力关闭

线程池的状态变为 STOP。线程池会终止当前正在运行的任务,停止处理排队的任务,并返回正在等待执行的任务List。

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

5.3 awaitTerminatio()阻塞关闭

关闭线程池嘛,肯定也得有个线程来操作这个事情,比如我们的主线程。

上面两个是平滑关闭和暴力关闭,然后啊,这两个操作相对于主线程是异步的在通知线程池,你该关闭了,意思就是不会阻塞主线程。

你看,一个返回void一个返回List待执行任务。

假设我们就想站在这里等这个线程池关闭,需要用到awaitTermination()方法。效果就是主线程阻塞等待直到线程池的所有任务都执行完毕,并且所有的工作线程都被终止。这样可以确保在主线程继续执行之前,线程池已经完全关闭了。

调用 awaitTermination 方法时,你可以设置一个超时时间,以避免主线程长时间阻塞。如果超时时间到达而线程池依然没有关闭完成,awaitTermination 方法会返回 false。另外,由于该方法可能会抛出 InterruptedException 异常,你需要在调用时进行异常处理,以确保程序的健壮性。

    public boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (;;) {
                if (runStateAtLeast(ctl.get(), TERMINATED))
                    return true;
                if (nanos <= 0)
                    return false;
                nanos = termination.awaitNanos(nanos);
            }
        } finally {
            mainLock.unlock();
        }
    }

6.获取任务执行结果

6.1 Future

通过提交 Callable 任务给线程池,线程池返回一个 Future 对象,可以用于异步获取任务的执行结果。可以使用 Future 的 get() 方法来阻塞等待任务完成,并获取任务的返回值。

import java.util.concurrent.*;

public class FutureExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newFixedThreadPool(1);
        
        Future<Integer> future = executor.submit(new Callable<Integer>() {
            public Integer call() throws Exception {
                // 模拟任务执行
                Thread.sleep(1000);
                return 42;
            }
        });
        
        // 阻塞等待任务完成,并获取结果
        Integer result = future.get();
        System.out.println("Future 获取任务执行结果:" + result);
        // 关闭线程池
        executor.shutdown();
    }
}

6.2 CompletionService

CompletionService是 ExecutorService 的一个扩展接口,用于管理已完成的任务。

通过将任务提交给 CompletionService,可以异步地获取任务的执行结果,并且可以按照完成顺序获取结果。

import java.util.concurrent.*;

public class CompletionServiceExample {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executor = Executors.newFixedThreadPool(1);
        
        CompletionService<Integer> completionService = new ExecutorCompletionService<>(executor);
        
        completionService.submit(new Callable<Integer>() {
            public Integer call() throws Exception {
                // 模拟任务执行
                Thread.sleep(1000);
                return 42;
            }
        });
        
        // 获取任务执行结果
        Future<Integer> future = completionService.take();
        Integer result = future.get();
        System.out.println("CompletionService 获取任务执行结果:" + result);
        // 关闭线程池
        executor.shutdown();
    }
}

6.3 自定义数据结构

可以设计自定义的数据结构来保存任务的执行结果。

例如,使用 ConcurrentHashMap 来保存任务的返回值,任务执行完成后将结果存储在 Map 中,其他线程可以通过键来获取对应的结果。

import java.util.concurrent.*;

public class CustomDataStructureExample {
    public static void main(String[] args) throws InterruptedException {
        
        ExecutorService executor = Executors.newFixedThreadPool(1);
        
        ConcurrentHashMap<String, Integer> resultMap = new ConcurrentHashMap<>();
        
        executor.submit(new Runnable() {
            public void run() {
                // 模拟任务执行
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 保存结果到 Map 中
                resultMap.put("key", 42);
            }
        });
        
        // 等待一段时间,确保任务执行完成
        Thread.sleep(1500);
        // 从 Map 中获取结果
        Integer result = resultMap.get("key");
        System.out.println("自定义数据结构获取任务执行结果:" + result);
        // 关闭线程池
        executor.shutdown();
    }
}

7.常见问题

7.1 线程池为啥要用阻塞队列BlockingQueue?

  • 如果线程池中不使用阻塞队列,而是使用普通的非阻塞队列:

    • 队列为空时,尝试取出元素的操作会立即返回空值或者抛出异常,而不会阻塞线程。
    • 队列已满时,尝试放入元素的操作也会立即返回失败,可能会抛出异常或者丢弃任务,而不会等待队列有空余位置。

    线程池需要自己处理任务的等待和唤醒,增加了线程同步和管理的复杂度。

  • 使用了阻塞队列后

    • 队列为空时,尝试从队列中取出任务的操作会被阻塞,直到队列中有新的任务被添加进来。
    • 队列已满时,尝试往队列中放入新任务的操作也会被阻塞,直到队列中有空余位置可用。

    能有效地平衡生产者和消费者之间的速度差异,提高系统的整体效率,并且简化了线程同步和管理的逻辑。

7.2 线程之间如何传递参数?

7.2.1 共享变量

可以使用共享变量来在线程之间传递参数,一个线程设置变量的值,另一个线程读取该变量的值。需要注意线程安全的问题。

    public static void main(String[] args) throws ExecutionException {
        // 共享变量
        AtomicInteger sharedValue = new AtomicInteger();
        // 创建线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 提交线程1任务
        Future<?> future1 = executor.submit(() -> {
            // 线程1设置参数值
            sharedValue.set(42);
        });

        // 提交线程2任务
        Future<?> future2 = executor.submit(() -> {
            // 线程2读取参数值
            System.out.println("线程2读取参数值为:" + sharedValue);
        });

        // 等待线程1和线程2执行结束
        try {
            future1.get();
            future2.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        // 关闭线程池
        executor.shutdown();
    }

7.2.2 共享队列

嗯,和上面的差不多,也是共享数据,通过在线程之间共享内存来实现参数的传递。区别在于数据结构的选择和操作方式。

    public static void main(String[] args) throws ExecutionException {
        // 创建共享队列
        BlockingQueue<Integer> sharedQueue = new ArrayBlockingQueue<>(1);
        // 创建线程池
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 提交线程1任务
        Future<?> future1 = executor.submit(() -> {
            try {
                // 线程1设置参数值
                sharedQueue.put(42);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 提交线程2任务
        Future<?> future2 = executor.submit(() -> {
            try {
                // 线程2读取参数值
                System.out.println("线程2读取参数值为:" + sharedQueue.take());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        // 等待线程1和线程2执行结束
        try {
            future1.get();
            future2.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        // 关闭线程池
        executor.shutdown();
    }

7.3 线程池的提交时的submit和execute

关键点在于,是否需要任务的结果。

  • execute:适用于提交不需要返回结果的任务,比如日志记录或发送通知。

  • submit:适用于提交需要返回结果或需要知道任务是否完成的任务。

特性 execute submit
所属类 interface Executor interface ExecutorService extends Executor
返回类型 void Future<T>
接受的任务类型 Runnable RunnableCallable<T>
异常处理 通过 Thread.UncaughtExceptionHandler 处理 异常被封装在返回的 Future
是否需要返回结果
用途 适用于提交不需要返回结果的任务 适用于提交需要返回结果或需要知道任务是否完成的任务
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        
        // execute
        Runnable task = () -> System.out.println("Executing task");
        executorService.execute(task);

        // submit
        Future<String> future = executorService.submit(() -> "结果");
        String result = future.get(); // 获取结果
        System.out.println(result);
    }

8.面试题

今天是2024.5.10,我在xx网上收集了200页的后端面经文章,然后把涉及到线程池字眼的问题拿出来了。

找gpt整理后,结果如下。扫了眼gpt还是漏了很多,需要源文本的话,可以私信我。

仅供参考


  1. 线程池的执行流程是怎样的?
  2. 线程池的拒绝策略能否自定义?线程池有哪些拒绝策略?
  3. 怎么设计线程池?
  4. 线程池的七个参数是什么?线程池的参数和执行流程?
  5. 为什么要使用线程池?使用线程池有什么好处?
  6. 线程池怎么实现?线程池的底层原理是什么?
  7. 线程池为什么要先放满阻塞队列再申请空闲线程,而不是直接创建到最大线程数?
  8. 使用线程池操作hashmap是线程安全的吗?
  9. 线程池中阻塞队列有哪几种类型?
  10. 手撕线程池是怎样的过程?
  11. 线程池大小数量是不是越大越好?
  12. 怎么设置线程池的数量?
  13. 为什么不使用Executors创建线程池?
  14. 线程池选择线程的方法是什么?
  15. 线程池与多核心有什么关系?
  16. 线程池的阻塞队列有哪些实现方式?
  17. 创建线程池的时候一般怎么初始化?
  18. 线程池的核心参数 最大线程数和核心线程数区别 什么时候核心线程数的数量达到最大线程数?
  19. 线程池有哪几种创建方式?
  20. 线程池的核心参数?
  21. 线程池有了解吗,了解ThreadPoolExecutor?
  22. 线程池池化技术,与多线程技术比有什么好处?
  23. 线程池里边有队列,队列满了之后,如何处理新进入的任务?
  24. 线程池的状态是怎样的?
  25. 介绍线程池的核心参数?
  26. 线程池怎么限流的?
  27. 在使用线程池的时候使用ThreadLocal会有什么的问题?
  28. 线程池创建线程的方法?
  29. 线程池的运行机制是什么?
  30. 线程池拒绝策略 什么时候拒绝?
  31. 线程池是怎么实现的?具体流程?
  32. 线程池如何使用?参数的含义是什么?
  33. 线程池的好处是什么?你项目中用了哪个线程池?
  34. 线程池核心数怎么确定的?
  35. 项目中对线程池的使用还能优化吗?
  36. 说一下线程池,有哪些参数?核心线程数由什么决定如何设置?
  37. 线程池怎样停止?
  38. 怎么实现Linux线程池?
  39. 你在项目中是怎么使用线程池的?
  40. 线程池有哪些分类,线程池创建参数有哪些?
  41. 线程池的任务队列如何实现,空闲线程用什么策略拉取队列的任务?
  42. 如何创建线程?多线程了解吗?线程池参数?怎么设置一般?
posted @ 2024-05-09 16:09  羊37  阅读(23)  评论(0编辑  收藏  举报