Java并发编程面试必备:如何创建线程池、线程池拒绝策略

news2024/9/20 14:27:20

一、线程池

1. 线程池使用

1.1 如何配置线程池大小

如何配置线程池大小要看业务系统执行的任务更多的是计算密集型任务,还是I/O密集型任务。大家可以从这两个方面来回答面试官。

(1)如果是计算密集型任务,通常情况下,CPU个数为N,设置N + 1个线程数量能够实现最优的资源利用率。因为N + 1个线程能保证至少有N个线程在利用CPU,提高了CPU利用率;同时不设置过多的线程也能减少线程状态切换所带来的上下文切换消耗。

(2)如果是I/O密集型任务,线程的主要等待时间是花在等待I/O操作上,另外就是计算所花费的时间。一般可以根据这个公式得出线程池合适的大小配置。 $$ 线程池大小 = CPU数量 * CPU期望的利用率 * (1 + IO操作等待时间/CPU计算时间) $$

1.2 创建线程池

我们可以使用ThreadPoolExecutor自定义创建线程池,这也是创建线程池推荐的创建方式。

public ThreadPoolExecutor(int corePoolSize, // 要保留在池中的线程数
                          int maximumPoolSize, // 池中允许的最大线程数
                          long keepAliveTime, // 当线程数大于corePoolSize时,多余的空闲线程在终止之前等待新任务的最长时间
                          TimeUnit unit, // 时间单位
                          BlockingQueue<Runnable> workQueue, // 在执行任务之前用于保存任务的队列
                          ThreadFactory threadFactory) { // 执行程序创建新线程时使用的工厂
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}

另外Executors类也提供了一些静态工厂方法,可以用来创建一些预配置的线程池。

newFixedThreadPool可以设置线程池的固定线程数量。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

newSingleThreadExecutor可以让线程按序执行,适用于需要确保所有任务按序执行的场景。

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

大家看下以下源码,newCachedThreadPool的线程数没有上限限制,同时空闲线程的存活时间是60秒。newCachedThreadPool更适合系统负载不太高、线程执行时间短的场景下,因为线程任务不需要经过排队,直接交给空闲线程就可以。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

newScheduledThreadPool可以安排任务在给定的延迟后运行,或者定期执行。

public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}

1.3 预配置线程池弊端

小伙伴要记得上述静态工厂方法在使用过程中可能会出现OOM内存溢出的情况。

  1. newFixedThreadPoolnewSingleThreadExecutor:因为线程池指定的请求队列类型是链表队列LinkedBlockingQueue<Runnable>(),故允许的请求队列长度是无上限的,可能会出现OOM内存溢出。
  2. newCachedThreadPoolnewScheduledThreadPool:线程池指定的线程数上限是Integer.MAX_VALUE,故允许创建的线程数量是无上限的Integer.MAX_VALUE,可能会出现OOM内存溢出。

1.3 Spring创建线程池

一般Spring工程创建线程池不直接使用ThreadPoolExecutor。

Spring框架提供了以Bean形式来配置线程池的ThreadPoolTaskExecutor类,ThreadPoolTaskExecutor类的底层实现还是基于JDK的ThreadPoolExecutor。

# 示例代码
@Bean(name = "testExecutor")
public ThreadPoolTaskExecutor testExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    // 配置核心线程数
    executor.setCorePoolSize();
    // 配置最大线程数
    executor.setMaxPoolSize();
    // 配置队列大小
    executor.setQueueCapacity();
    executor.initialize();
    return executor;
}

2. 线程池拒绝策略

大家如果有看ThreadPoolExecutor源码就知道,ThreadPoolExecutor类实现了setRejectedExecutionHandler方法,顾名思义意思是设置拒绝执行处理程序。

# ThreadPoolExecutor源码
/**
* Sets a new handler for unexecutable tasks. // 为无法执行的任务设置新的处理程序
*
* @param handler the new handler
* @throws NullPointerException if handler is null
* @see #getRejectedExecutionHandler
*/
public void setRejectedExecutionHandler(RejectedExecutionHandler handler) {
    if (handler == null)
        throw new NullPointerException();
    this.handler = handler;
}

该方法可以为线程池设置拒绝策略,目前JDK8一共有四种拒绝策略,也对应入参RejectedExecutionHandler的四种子类实现。

  1. AbortPolicy:默认的拒绝策略,直接抛出RejectedExecutionException异常。
  2. CallerRunsPolicy:直接在execute方法的调用线程中运行被拒绝的任务。
  3. DiscardPolicy:直接丢弃被拒绝的任务。
  4. DiscardOldestPolicy:丢弃最旧的未处理请求,然后重试execute 。

另外如果线程池拒绝策略设置为DiscardOldestPolicy,线程池的请求队列类型最好不要设置为优先级队列PriorityBlockingQueue。因为该拒绝策略是丢弃最旧的请求,也就意味着丢弃优先级最高的请求

3. 线程工厂的作用

ThreadFactory定义了创建线程的工厂,回答这个问题我们就要结合实际场景了。

ThreadFactory线程工厂能够为线程池里每个线程设置名称、同时设置自定义异常的处理逻辑,可以方便我们通过日志来定位bug的位置。

以下是一个代码示例。

@Slf4j
public class CustomGlobalException {
    public static void main(String[] args) {
        ThreadFactory factory = r -> {
            String threadName = "线程A";
            Thread thread = new Thread(r, threadName);
            thread.setUncaughtExceptionHandler((t, e) -> {
                log.error("{}执行了自定义异常日志", threadName);
            });
            return thread;
        };
        ExecutorService executor = new ThreadPoolExecutor(6,
                6,
                0,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(66),
                factory);

        executor.execute(() -> {
            throw new NullPointerException();
        });
        executor.shutdown();
    }
}

控制台打印:2024-04-26 22:04:45[ ERROR ]线程A执行了自定义异常日志

2. 线程通信

2.1 线程的等待/通知机制

Java线程的等待/通知机制指的是:线程A获得了synchronized同步方法、同步方法块的锁资源后,调用了锁对象的wait()方法,释放锁的同时进入等待状态;而线程B获得锁资源后,再通过锁对象的notify()或notifyAll()方法来通知线程A恢复执行逻辑。

其实Java的所有对象都拥有等待/通知机制的本领,大家可以在JDK源码package java.lang`下找到Java.lang.Object里提供的五个与等待/通知机制相关的方法。

一、等待。

(1)使当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法。

    public final void wait() throws InterruptedException {
        wait(0);

(2)使当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或者指定的毫秒timeout过去。

  public final native void wait(long timeout) throws InterruptedException;

(3)使当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或者指定的毫秒timeout过去,另外nanos是额外时间,以纳秒为单位。

    public final void wait(long timeout, int nanos) throws InterruptedException {
    }

所以其实wait()、watit(0)、watit(0, 0)执行后都是同样的效果

二、通知。

(1)唤醒在此对象监视器上等待的单个线程。

    public final native void notify();

(2)唤醒在此对象监视器上等待的所有线程。

    public final native void notifyAll();

大家有没听说过消费者生产者问题呢?消费者生产者之间要无限循环生产和消费物品,解决之道就是两者形成完美的等待、通知机制。而这套机制就可以通过上文的wait、notify方法来实现。

2.2 线程通信方式

(1)利用Condition进行线程通信。

如果大家的程序直接采用的是Lock对象来同步,则没有了上文synchronized锁带来的隐式同步器,也就无法使用wait()、notify()方法。

此时的线程可以使用Condition对象来进行通信。例如下文的示例代码: condition0的await()阻塞当前线程,同时释放、等待获取锁资源;接着等待其他线程调用condition0的signal()来通知其获取锁资源继续执行。

@Slf4j
public class UseReentrantLock {

    private static final ReentrantLock lock = new ReentrantLock();

    private static final Condition condition0 = lock.newCondition();

    private static final Condition condition1 = lock.newCondition();

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                lock.lock();

                for (int i = 1; i < 4; i++) {
                    log.info(i + "");

                    condition1.signal();
                    condition0.await();
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();

        new Thread(() -> {
            try {
                lock.lock();

                for (int i = 65; i < 68; i++) {
                    log.info((char) i + "");

                    condition0.signal();
                    condition1.await();
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }).start();
    }
}
# 程序执行结果
2024-05-30 10:30:30[ INFO ]1
2024-05-30 10:30:30[ INFO ]A
2024-05-30 10:30:30[ INFO ]2
2024-05-30 10:30:30[ INFO ]B
2024-05-30 10:30:30[ INFO ]3
2024-05-30 10:30:30[ INFO ]C

(2)Thread采用join方法进行通信。

线程Thread对象还提供了join方法,也是一种通信的方式。当某个程序的执行流调用了某个thread对象的join方法,调用线程将会被阻塞,等到thread对象终止后才通知调用线程继续执行。

    public final void join() throws InterruptedException {
        join(0);
    }

(3)volatile共享变量。

volatile的出现,大家是不是有些意外呢?虽然volatile适用的多线程场景不多,但它也是线程通信的一种方式。被volatile修饰的变量如果更新了值,则会通过主内存这条消息总线通知所有使用该变量的线程,让其把主内存同步到工作内存里,则所有线程都会获取共享变量最新值。

2.3 更加灵活的ReentrantLock

在线程同步上,JDK的Lock接口提供了多个实现子类,如下所示。下面我按面试官面试频率高的ReentrantLock来讲解。

在这里插入图片描述

ReentrantLock相比synchronized来说使用锁更加灵活,可以自由进行加锁、释放锁。ReentrantLock类提供了lock()、unlock()来实现以上操作。具体实操代码可以看上一个面试官问题关于Condition的示例代码。

// ReentrantLock源码
package java.util.concurrent.locks;
public class ReentrantLock implements Lock, java.io.Serializable {
    
    // 获取锁
    public void lock() {
        sync.lock();
    }
    
    // 尝试释放此锁
    public void unlock() {
        sync.release(1);
    }
}

另外ReentrantLock和synchronized都是可重入锁,即线程获取锁资源后,下一步如果进入相同锁资源的同步代码块,不需要再获取锁。

ReentrantLock也可以实现公平锁,即成功获取锁的顺序与申请锁资源的顺序一致。我们在创建对象时进行初始化设置就可以设置为公平锁。

 ReentrantLock lock = new ReentrantLock(true);

3. ThreadLocal作用

上文我们讨论的都是在多个线程对共享资源进行通信的业务场景上,例如商城业务秒杀的库存要保证数据安全性。而如果在多个线程对共享资源进行线程隔离的业务场景上,则可以使用ThreadLoccal来解决。

ThreadLocal可以保存当前线程的副本值,提供了set、get方法,通过set方法可以把指定值设置到当前线程副本;而通过get方法可以返回此当前线程副本中的值。

例如要实现一个功能,每个线程打印当前局部变量:局部变量 + 10,我们就可以利用ThreadLocal保存共享变量i,来避免对变量i的共享冲突。

public class UseThreadLocal {
    public static void main(String[] args) {
        ExecutorService es = Executors.newFixedThreadPool(20);
        for (int i = 0; i < 3; i++) {
            int number = i;
            es.execute(() -> System.out.println(number + ":" + new intUtil().addTen(number)));
        }
    }

    private static class intUtil {
        public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); // 使用threadLocal保存线程保存的当前共享变量num

        public static int addTen(int number) {
            threadLocal.set(number);

            try { // 休息1秒
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return threadLocal.get() + 10;
        }
    }
}
# 程序执行结果
0:10
2:12
1:11

4. 线程生命周期

线程生命周期可以大致分为六步,如下所示。

  1. 初始状态。创建了线程对象还没有调用start()。

  2. 就绪或运行状态。执行了start()可能运行,也可能进入就绪状态在等待CPU资源。

  3. 阻塞状态 。一直没有获得锁。

  4. 等待状态。等待其他线程的通知唤醒。

  5. 超时状态。

  6. 终止状态。

二、并发编程面试题

1. 什么是线程池?为什么要使用线程池?

回答:
线程池是一种复用已创建线程的技术,通过提前创建一组线程并将它们存储在池中,可以在需要执行任务时直接从池中获取线程,而不需要每次都创建新的线程。这种机制可以减少线程的创建和销毁带来的性能开销,提高系统响应速度,并且通过合理配置线程池的大小,可以有效控制并发数量,避免系统资源耗尽。

使用线程池有以下几个主要优点:

  • 降低资源消耗: 通过重复利用线程池中的线程,减少了频繁创建和销毁线程的开销。
  • 提高响应速度: 线程池可以在接收到任务时直接使用现有线程,减少了创建线程的时间。
  • 提高线程管理能力: 线程池可以根据系统的负载自动调节线程的数量,避免过多线程带来的资源争夺。
  • 增强系统稳定性: 通过控制最大并发线程数,避免因创建过多线程导致系统崩溃。

2. Java中如何创建一个线程池?简述常用的ThreadPoolExecutor构造方法。

回答:
Java中可以通过 ThreadPoolExecutor 来创建一个自定义的线程池。ThreadPoolExecutor 的构造方法如下:

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

各参数含义:

  • corePoolSize: 核心线程数,表示线程池中始终保持存活的线程数,即使它们处于空闲状态。
  • maximumPoolSize: 最大线程数,表示线程池中允许的最大线程数。
  • keepAliveTime: 非核心线程空闲时的存活时间。当线程数超过 corePoolSize 时,多余的空闲线程会在等待新任务的时间超过 keepAliveTime 后被终止。
  • unit: keepAliveTime 的时间单位。
  • workQueue: 任务队列,用于存储等待执行的任务。
  • threadFactory: 线程工厂,用于创建新线程。
  • handler: 拒绝策略,在线程池饱和时用于处理无法执行的任务。

3. Java提供了哪些内置的线程池实现?它们的适用场景分别是什么?

回答:
Java 提供了以下几种常用的线程池实现:

  • newFixedThreadPool(int nThreads): 创建一个固定大小的线程池,核心线程数和最大线程数相同,适用于需要控制并发数量的场景,比如处理固定数量的任务。
  • newCachedThreadPool(): 创建一个缓存线程池,可以根据需要自动调整线程池大小,适用于执行大量短期任务的场景。
  • newSingleThreadExecutor(): 创建一个单线程的线程池,适用于需要保证任务顺序执行的场景。
  • newScheduledThreadPool(int corePoolSize): 创建一个定时调度的线程池,适用于需要定时或周期性执行任务的场景。

4. newFixedThreadPoolnewCachedThreadPool 的区别是什么?它们适用于哪些场景?

回答:
newFixedThreadPoolnewCachedThreadPool 是两种不同的线程池实现,主要区别如下:

  • newFixedThreadPool:

    • 固定大小: 线程池大小固定,即核心线程数和最大线程数相同,且线程池中线程不会超出指定数量。
    • 任务队列: 使用无界的 LinkedBlockingQueue 存储待执行的任务。
    • 适用场景: 适合处理稳定、长期的任务,或者需要限制并发线程数量的场景,如对外部资源(数据库、文件等)进行有限制访问的场景。
  • newCachedThreadPool:

    • 可扩展大小: 线程池大小动态调整,线程池中线程数量可以根据需要不断增加,空闲线程在 60 秒后会被回收。
    • 任务处理: 当线程池中有空闲线程时会复用,若没有空闲线程则会创建新线程。
    • 适用场景: 适合处理大量、短期的任务,或任务执行时间较短且不频繁的场景,如处理高并发请求。

5. ThreadPoolExecutor 的核心参数有哪些?分别起到什么作用?

回答:
ThreadPoolExecutor 的核心参数有以下几个:

  • corePoolSize: 核心线程数,即线程池中始终保持存活的线程数。
  • maximumPoolSize: 最大线程数,即线程池中允许的最大线程数。
  • keepAliveTime: 空闲线程存活时间,当线程数超过 corePoolSize 时,多余的空闲线程会在等待新任务的时间超过 keepAliveTime 后被终止。
  • unit: keepAliveTime 的时间单位。
  • workQueue: 任务队列,用于存储等待执行的任务。常见的有 LinkedBlockingQueueSynchronousQueueArrayBlockingQueue
  • threadFactory: 线程工厂,用于创建新线程,通常用于给线程设置一些属性,如名称、优先级等。
  • handler: 拒绝策略,在线程池及其任务队列都满时用于处理无法执行的任务。常见的策略有 AbortPolicyCallerRunsPolicyDiscardPolicyDiscardOldestPolicy

6. 如何设置线程池的核心线程数和最大线程数?如何判断合适的线程数?

回答:
线程池的核心线程数和最大线程数的设置取决于任务的类型和系统的硬件资源。一般来说,核心线程数 corePoolSize 应该设置为能够同时处理的并发任务数量,而最大线程数 maximumPoolSize 则设置为系统能够承受的最大并发任务数量。

常见的设置方法如下:

  • CPU密集型任务: 对于CPU密集型任务(如计算、数据处理),通常建议设置核心线程数为CPU核心数+1,最大线程数可以略高于核心数。公式:corePoolSize = CPU核心数 + 1
  • I/O密集型任务: 对于I/O密集型任务(如文件读写、网络通信),由于线程在等待I/O操作完成时会处于空闲状态,因此可以设置更多的线程来提高并发度。公式:corePoolSize = CPU核心数 * 2 或更多。

在设置时,建议通过性能测试来验证具体的设置是否合适,并根据实际情况进行调整。

7. 什么是线程池的拒绝策略?Java中有哪些内置的拒绝策略?

回答:
线程池的拒绝策略是在线程池和任务队列都满时,无法处理新提交任务时采取的处理措施。Java中的ThreadPoolExecutor 提供了几种内置的拒绝策略:

  • AbortPolicy: 默认拒绝策略,直接抛出 RejectedExecutionException 异常,阻止任务继续执行。
  • CallerRunsPolicy: 调用者运行策略,将任务回退到调用者线程执行,以降低提交新任务的速度。
  • DiscardPolicy: 丢弃策略,直接丢弃无法处理的新任务,不做任何处理也不抛出异常。
  • DiscardOldestPolicy: 丢弃最旧策略,丢弃任务队列中最旧的任务,然后尝试重新提交新任务。

8. 当任务提交速率超过线程池处理能力时,线程池会发生什么?如何处理这种情况?

回答:
当任务提交速率超过线程池处理能力时,线程池的任务队列会逐渐堆积,最终达到最大容量。这时,线程池将触发拒绝策略来处理无法执行的新任务。

处理这种情况的方法包括:

  • 增加线程池大小: 增加 maximumPoolSize,允许更多线程并发执行任务,缓解任务堆积的问题。
  • 使用无界队列: 使用无界队列(如 LinkedBlockingQueue),允许任务无限堆积(注意,这可能会导致内存溢出)。
  • 优化任务执行时间: 优化任务代码,减少每个任务的执行时间,从而提升线程池处理速度。
  • 使用拒绝策略: 使用合适的拒绝策略,如 CallerRunsPolicy,让调用者线程执行任务,防止任务队列继续堆积。

9. 什么是 CallerRunsPolicy 拒绝策略?它在什么情况下适用?

回答:
CallerRunsPolicy 是一种线程池的拒绝策略,当线程池和任务队列都满时,线程池不会抛出异常或丢弃任务,而是将任务回退给调用者线程执行。这种策略通过减缓新任务的提交速度来防止线程池任务堆积过多。

CallerRunsPolicy 适用于以下情况:

  • 流量控制: 当系统流量突然增加时,通过让调用者线程执行任务,减缓任务提交速度,避免线程池超载。
  • 任务必须执行: 在某些情况下,任务不能被丢弃或忽略,因此可以使用 CallerRunsPolicy 确保每个任务都能被执行。

10. 如何自定义线程池的拒绝策略?在什么情况下需要自定义?

回答:
自定义线程池的拒绝策略可以通过实现 RejectedExecutionHandler 接口来完成。具体步骤如下:

public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 自定义拒绝处理逻辑
        // 比如记录日志、通知运维、重试任务等
    }
}

然后在创建 ThreadPoolExecutor 时将其作为参数传入:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,
    maximumPoolSize,
    keepAliveTime,
    TimeUnit.SECONDS,
    workQueue,
    new CustomRejectedExecutionHandler()
);

自定义拒绝策略适用于以下情况:

  • 特殊业务需求: 当系统有特殊的业务需求,必须对拒绝的任务进行特定处理时,可以自定义策略。
  • 复杂流量控制: 如果内置的拒绝策略不能满足系统的流量控制需求,可以通过自定义策略实现更复杂的流量控制逻辑。

11. 线程池中的任务队列是如何工作的?SynchronousQueueLinkedBlockingQueue 有什么区别?

回答:
线程池中的任务队列用于存储等待执行的任务,常见的任务队列有:

  • SynchronousQueue: 是一个无容量的队列,每个插入操作必须等待相应的移除操作。这意味着提交的任务不会被实际存储,而是直接传递给线程执行。如果没有可用线程,任务会被拒绝。因此,SynchronousQueue 适用于任务执行时间短且系统负载高的场景,能够快速地将任务交给空闲线程执行。

  • LinkedBlockingQueue: 是一个基于链表的无界队列,可以存储大量待执行的任务。如果线程池中的线程全部繁忙,新的任务会被存放在队列中等待执行。LinkedBlockingQueue 适用于任务提交速率较高但执行时间较长的场景,能够有效缓解线程池压力。

两者的区别在于:

  • 容量: SynchronousQueue 没有容量,而 LinkedBlockingQueue 是一个有容量限制的队列,默认是无界的。
  • 适用场景: SynchronousQueue 适合于任务较快、需要高吞吐量的场景;LinkedBlockingQueue 适合于任务执行时间长、需要排队的场景。

12. 线程池中的线程是如何被复用的?为什么线程池能够提高性能?

回答:
线程池通过复用线程来提高性能。当线程池接收到新任务时,会尝试使用池中已有的空闲线程来执行任务,而不是每次都创建新的线程。线程在执行完任务后,不会被销毁,而是重新回到池中,等待执行下一个任务。

线程池能够提高性能的原因有:

  • 减少线程创建销毁的开销: 线程的创建和销毁是有成本的,频繁的创建和销毁会浪费CPU资源。通过复用线程,可以减少这些开销。
  • 控制并发数量: 线程池可以限制同时执行的线程数量,避免系统因过多线程而资源耗尽或陷入死锁。
  • 提高系统稳定性: 通过合理的线程管理,线程池能够确保系统在高负载下仍能稳定运行,避免因资源耗尽导致的系统崩溃。

13. ThreadPoolExecutorkeepAliveTime 参数的作用是什么?如何调整这个参数?

回答:
keepAliveTime 参数定义了线程池中的非核心线程在没有任务执行时等待新任务的最长时间。如果超过这个时间且没有新任务到来,线程会被终止。这有助于减少线程池中不必要的线程资源占用。

调整 keepAliveTime 参数的建议:

  • 短期任务: 如果任务执行时间短且频繁,可以将 keepAliveTime 设置得较短,以便快速回收空闲线程。
  • 长时间运行的任务: 如果任务执行时间长或任务到来的间隔时间较长,可以适当增加 keepAliveTime,避免频繁创建和销毁线程。

可以通过 ThreadPoolExecutorsetKeepAliveTime 方法动态调整这个参数:

executor.setKeepAliveTime(10, TimeUnit.SECONDS);

14. 如何优雅地关闭线程池?shutdownshutdownNow 方法的区别是什么?

回答:
要优雅地关闭线程池,可以使用以下方法:

  • shutdown(): 发起线程池的平滑关闭操作。此方法会停止接收新任务,并等待已提交的任务执行完毕。shutdown() 方法不会强制终止正在执行的任务,而是让它们执行完后关闭线程池。

  • shutdownNow(): 立即关闭线程池。此方法会尝试取消所有正在执行的任务,并返回正在等待执行的任务列表。shutdownNow() 会强制终止所有活动的任务,但可能无法保证所有任务都被成功终止。

区别在于:

  • shutdown: 平滑关闭,等待任务执行完毕,适用于大多数情况下的正常关闭。
  • shutdownNow: 强制关闭,适用于紧急情况或不再需要完成正在执行的任务时。

15. 如果在 ThreadPoolExecutor 中提交了一个任务,但任务抛出异常,会发生什么?如何捕获和处理这些异常?

回答:
ThreadPoolExecutor 中,提交的任务如果抛出异常,该异常会被 ThreadPoolExecutor 捕获并直接抛弃,不会影响其他任务的执行。默认情况下,这些异常不会被重新抛出,也不会被日志记录。

处理这些异常的方式包括:

  • 在任务内部捕获异常: 可以在提交的 RunnableCallable 任务内部用 try-catch 块捕获并处理异常。
executor.submit(() -> {
    try {
        // 任务逻辑
    } catch (Exception e) {
        // 异常处理
    }
});
  • 自定义 ThreadFactory: 可以创建一个自定义的 ThreadFactory,为线程设置 UncaughtExceptionHandler,以便捕获线程中的未处理异常。
ThreadFactory threadFactory = runnable -> {
    Thread thread = new Thread(runnable);
    thread.setUncaughtExceptionHandler((t, e) -> {
        // 处理未捕获的异常
    });
    return thread;
};
  • 使用 Future 获取异常: 如果任务是通过 submit 提交的,可以通过返回的 Future 获取任务执行过程中抛出的异常。
Future<?> future = executor.submit(task);
try {
    future.get();
} catch (ExecutionException e) {
    // 处理任务抛出的异常
} catch (InterruptedException e) {
    // 处理线程被中断的情况
}

16. 如何监控线程池的状态?有哪些常见的方法来获取线程池的运行时信息?

回答:
监控线程池的状态可以帮助了解线程池的工作情况,并根据需要进行调整。常见的监控方法包括:

  • 获取线程池状态:

    • getPoolSize(): 返回线程池中当前的线程数,包括空闲线程。
    • getActiveCount(): 返回正在执行任务的线程数。
    • getTaskCount(): 返回线程池已经处理和正在处理的任务总数。
    • getCompletedTaskCount(): 返回已经完成执行的任务数。
    • getQueue().size(): 返回任务队列中的等待任务数。
  • 设置钩子方法:

    • 可以通过重写 ThreadPoolExecutorbeforeExecuteafterExecute 方法,在任务执行前后记录状态信息,甚至处理异常。
@Override
protected void beforeExecute(Thread t, Runnable r) {
    super.beforeExecute(t, r);
    // 任务执行前的监控逻辑
}

@Override
protected void afterExecute(Runnable r, Throwable t) {
    super.afterExecute(r, t);
    // 任务执行后的监控逻辑
}
  • 使用JMX监控: Java 提供了 JMX(Java Management Extensions),可以通过它来远程监控线程池的状态。通过注册 ThreadPoolExecutorMBean,可以在JMX控制台中查看线程池的各种状态信息。

17. Executors 类提供了哪些方法来创建常见类型的线程池?

回答:
Executors 类提供了以下常见方法来创建不同类型的线程池:

  • newFixedThreadPool(int nThreads): 创建一个固定大小的线程池,线程数为 nThreads。适用于执行长期稳定的任务。

  • newCachedThreadPool(): 创建一个缓存线程池,线程池大小会根据需要自动调整,适用于大量短期任务的场景。

  • newSingleThreadExecutor(): 创建一个单线程的线程池,适用于需要顺序执行任务的场景。

  • newScheduledThreadPool(int corePoolSize): 创建一个定时调度的线程池,适用于需要定期执行任务的场景。

18. 如何避免线程池中的线程过多导致的资源耗尽?

回答:
为了避免线程池中的线程过多导致系统资源耗尽,可以采取以下措施:

  • 合理设置 maximumPoolSize: 根据系统的硬件资源(如CPU、内存)和任务特性(如CPU密集型、I/O密集型)来设置线程池的最大线程数。

  • 使用有界队列: 使用有界的任务队列(如 ArrayBlockingQueue)限制待执行任务的数量,避免任务队列过多堆积。

  • 监控和调整: 通过监控线程池的运行状态,动态调整线程池的大小和任务队列的容量。

  • 设置合理的拒绝策略: 选择合适的拒绝策略,在系统负载过高时拒绝新任务,避免资源耗尽。

  • 优化任务代码: 优化任务的执行效率,减少任务执行时间,从而提高线程池的处理能力。

19. 什么是线程饥饿死锁?如何避免?

回答:
线程饥饿死锁是指当线程池中的所有线程都在等待彼此完成某项任务时,导致线程池无法继续执行其他任务,最终所有线程陷入等待状态,系统出现死锁。

避免线程饥饿死锁的方法包括:

  • 避免任务之间的相互依赖: 尽量减少任务之间的相互依赖,确保每个任务可以独立完成。

  • 使用合理的线程池大小: 确保线程池中的线程数量足以处理所有可能的依赖关系,避免因线程数量不足导致的死锁。

  • 避免长时间锁定: 在任务中避免长时间持有锁,或者在等待锁时设置超时时间,以减少死锁的可能性。

20. 在并发环境中,如何设计一个健壮的线程池管理机制?

回答:
设计一个健壮的线程池管理机制需要考虑以下几个方面:

  • 合理配置线程池参数: 根据系统的具体需求和硬件资源合理配置 corePoolSizemaximumPoolSizekeepAliveTime 和任务队列大小。

  • 动态调整线程池: 通过监控线程池的状态,根据系统负载动态调整线程池的参数,以应对流量的波动。

  • 有效的拒绝策略: 配置合适的拒绝策略,在系统高负载时能够有效处理无法执行的任务。

  • 任务优先级管理: 设计任务优先级机制,确保高优先级任务能够及时执行。

  • 任务超时处理: 设置任务执行的超时时间,避免长时间执行的任务占用线程资源。

  • 日志和报警机制: 在任务执行过程中记录关键日志,并在出现异常或系统负载过高时触发报警,以便及时处理问题。

通过以上措施,可以设计出一个高效、稳定、灵活的线程池管理机制,确保系统在高并发环境下的稳定运行。

让我们一起学习,一起进步!期待在评论区与你们见面。

祝学习愉快!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2095602.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

模型 ACT心理灵活六边形

系列文章 分享 模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。接纳现实&#xff0c;灵活行动&#xff0c;追求价值。 1 ACT心理灵活六边形的应用 1.1 应对工作压力 背景&#xff1a; 在高压的工作环境中&#xff0c;员工经常面临巨大的工作压力&#xff0c;这可…

在VScode中使用Git将本地已有文件夹提交到Github仓库以便于使用版本控制进行项目开发

前置软件 VScode、Git。 Linux系统中安装Git工具请自行百度。可以通过git --version查看对应Git版本号。 Github创建空白仓库 一定要注意创建空白仓库&#xff0c;不要包含任何文件&#xff0c;包括Readme.md文件也不能有。 上面的仓库名&#xff08;Repository name&#xff…

Kaggle克隆github项目+文件操作+Kaggle常见操作问题解决方案——一文搞定,以openpose姿态估计项目为例

文章目录 前言一、Kaggle克隆仓库1、克隆项目2、查看目录 二、安装依赖三、文件的上传、复制、转移操作1.上传.pth文件到input目录2、将权重文件从input目录转移到工作目录 三、修改工作目录里的文件内容1、修改demo_camera.py内容 四、运行&#xff01; 前言 想跑一些深度学习…

【网络安全】条件竞争绕过电子邮件验证

未经许可,不得转载。 文章目录 正文正文 目标:xxx.com 使用电子邮件注册该网站并登录。接着,进入帐户设置,进入更改电子邮件功能: 请求包如下: 接着,发送两个相同的请求包到repeater,第一个中添加攻击者邮件: 第二个中添加正常的邮件: 创建组,以便能够同时发送两个…

手把手教你如果安装激活CleanMyMac X 4.15.6中文破解版

CleanMyMac X 4.15.6中文破解版可以为Mac腾出空间&#xff0c;软件已经更新到CleanMyMac X 4.15.6中文版支持最新版Macos 10.14系统。CleanMyMac X 4.15.6中文破解版具有一系列巧妙的新功能&#xff0c;可让您安全&#xff0c;智能地扫描和清理整个系统&#xff0c;删除大量未使…

NeRF: Representing Scenes asNeural Radiance Fields for View Synthesis 论文解读

目录 一、导言 二、NeRF 1、渲染和反渲染 2、NeRF的基本原理 3、采样点 4、位置编码 5、NeRF网络结构 6、体渲染 三、分层采样 1、均匀采样 2、基于σ的采样 四、损失函数 一、导言 该论文来自于ECCV2020&#xff0c;主要提到一种NeRF的方法来合成复杂场景下的新视…

创建 AD9361 的 vivado 工程,纯FPGA配置,不使用ARM程序

前言 AD9361 的配置程序&#xff0c;如果使用官方的&#xff0c;就必须用ps进行配置&#xff0c;复杂不好使&#xff0c;如果直接使用FPGA配置&#xff0c;将会特别的简单。 配置软件 创建一份完整的寄存器配置表 //*******************************************************…

续:docker 仓库数据传输加密

上一个实验&#xff1a;非加密的形式在企业中是不被允许的。 示例&#xff1a;【为Registry 提供加密传输】 因为传输也是https&#xff0c;所以与ssh一样的加密。 ## 这种方式就不用写这个了。 [rootdocker ~]# cat /etc/docker/daemon.json #{ # "insecure-registrie…

GoodSync Business - 企业级服务器同步与备份工具

现在越来越多公司会搭建服务器&#xff0c;或自建文件共享中心。那么如何才能实现对这些终端的高效管理、安全备份&#xff0c;以保障企业数据的安全呢&#xff1f; GoodSync Business 就是一款企业服务器同步与备份工具&#xff0c;适用于 Win / Mac 工作站&#xff0c;以及 …

C语言程序设计

日落有个小商店&#xff0c;贩卖着橘黄色的温柔。 7.关系操作符 > > < < ! (用于测试“不相等”) &#xff08;用于测试“相等”&#xff0c;但是不是所有的对象都可以用该符号来比较相不相等&#xff09; eg. int main ( ) { if ("abc"&q…

【AI大模型】基于docker部署向量数据库Milvus和可视化工具Attu详解步骤

&#x1f680; 作者 &#xff1a;“大数据小禅” &#x1f680; 文章简介 &#xff1a;本专栏后续将持续更新大模型相关文章&#xff0c;从开发到微调到应用&#xff0c;需要下载好的模型包可私。 &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac; 目…

【王树森】Few-Shot Learning (2/3): Siamese Network 孪生网络(个人向笔记)

Learning Pairwise Similarity Scores Training Data 训练集有很多个类别的图片&#xff0c;每个类别的图片都有标注 Positive Sample&#xff1a;我们需要正样本来告诉神经网路什么东西是同一类 Negative Sample&#xff1a;负样本可以告诉神经网路事物之间的区别 我们用…

【笔记】数据结构04

文章目录 稀疏矩阵压缩存储三元组表示稀疏矩阵转三元组 栈的应用前缀表达式前缀表达式求值 中缀表达式后缀表达式 稀疏矩阵内容参考 强连通文章 稀疏矩阵压缩存储三元组表示 将非零元素所在的行、列以及它的值构成一个三元组&#xff08;i,j,v&#xff09;&#xff0c;然后再…

推荐 3 个好用的桌面管理工具,实用强大,不要错过

BitDock BitDock是一款运行在Windows系统中的桌面停靠栏工具&#xff0c;也被称为比特工具栏。这款软件模仿了Mac系统的Dock栏设计&#xff0c;旨在为用户提供一个简洁、高效且极具美感的桌面环境。BitDock不仅具有个性化的外观&#xff0c;还内置了多种实用功能&#xff0c;如…

Java后端 - 常见BUG及其处理策略(持续更新中~)

Bug 收集与总结 本文记录的是 本人SpringBoot 后端项目使用和运行代码时所遇到的各种问题&#xff0c;全部都已解决&#xff0c;欢迎在评论区补充你遇到的 Bug 哦&#xff01;仅以本文记录学习社区项目时&#xff0c;所遇到的奇奇怪怪的 bug&#xff0c;以及一些很愚蠢的错误&…

从马斯洛需求层次理论谈职场激励

马斯洛需求层次理论是一个广为人知的心理学概念&#xff0c;它是由美国心理学家亚伯拉罕马斯洛&#xff08;Abraham Maslow&#xff09;于1943年提出&#xff0c;常被描述为一个5层的金字塔模型&#xff0c;反映了人类不同需求层级和依赖关系。马斯洛认为人类只有在低层级需求得…

在 VS Code 中使用 Git 源代码管理【Mac 版】

文章目录 一、Git 使用文档二、使用示例1、复制远程仓库地址2、查看当前所在的分支2.1、界面查看2.2、终端查看 3、修改/新增文件4、显示增改的详细内容5、添加暂存区6、查看/取消暂存的更改7、提交本地代码库8、待提交文件9、推送到远程仓库10、验证11、查看推送记录11.1、关于…

六. 部署分类器-trt-engine-explorer

目录 前言0. 简述1. 案例运行2. 补充说明3. engine分析结语下载链接参考 前言 自动驾驶之心推出的 《CUDA与TensorRT部署实战课程》&#xff0c;链接。记录下个人学习笔记&#xff0c;仅供自己参考 本次课程我们来学习课程第六章—部署分类器&#xff0c;一起来学习 trt-engine…

Linux日志-secure日志

作者介绍&#xff1a;简历上没有一个精通的运维工程师。希望大家多多关注作者&#xff0c;下面的思维导图也是预计更新的内容和当前进度(不定时更新)。 Linux进阶部分又分了很多小的部分,我们刚讲完了Linux基础软件&#xff0c;下面是Linux日志。Linux 系统中的日志是记录系统活…

18041 分期还款(加强版)

### 自查思路 1. 检查输入数据的处理是否正确。 2. 检查判断条件 p < d * r 是否正确。 3. 确认公式计算和输出格式是否正确。 ### 伪代码 1. 读取输入的贷款金额、每月还款额和月利率。 2. 判断是否可以还清贷款&#xff1a; - 如果每月还款额小于贷款金额乘以月利率&a…