深度思考线程池面经之二:jdk线程池设计答疑

2 疑问

2.1 这里的“只有当线程池是无界的或者可以拒绝任务时,该队列才有实际价值”,为什么这么说

SynchronousQueue是一种特殊的队列,它不保留任务,而是直接将任务移交给工作线程。这种队列适合于执行大量生命周期非常短的异步任务。

当说“只有当线程池是无界的或者可以拒绝任务时,该队列才有实际价值”是因为:

无界线程池:SynchronousQueue没有存储任务的能力,所以每次添加任务都需要有一个可用的线程。如果线程池是无界的,那么每次添加任务时,都可以创建一个新的线程来处理这个任务。

拒绝策略:如果线程池已经达到其最大值并且所有线程都在忙,使用SynchronousQueue的话将无法存储更多的任务。这时,线程池必须有策略来拒绝新的任务,否则将会出现资源耗尽的问题。

使用SynchronousQueue的场景,如Executors.newCachedThreadPool,就是基于这样的原则:它不保留任何待处理的任务,但会根据需要创建新的线程,直到达到系统的最大限制。当线程空闲一定时间后,它会被终止和回收,所以这种线程池适用于执行大量生命周期非常短的异步任务。

2.2 线程池的maxPoolSize参数是动态可变的吗

在Java的java.util.concurrent.ThreadPoolExecutor中,maxPoolSize参数在初始化线程池时被设置,但在创建后也可以动态地进行修改。

你可以使用ThreadPoolExecutorsetMaxPoolSize(int maximumPoolSize)方法来在运行时动态地修改maxPoolSize。例如:

ThreadPoolExecutor executor = new ThreadPoolExecutor(...);
executor.setMaxPoolSize(newMaxPoolSize);

但是有几点需要注意:

  1. 增加maxPoolSize:如果你在运行时增加了maxPoolSize,并且当前的线程数低于新设置的maxPoolSize,线程池可能会因为队列中等待的任务而创建新的线程,直到线程数达到新的maxPoolSize或队列为空。

  2. 减少maxPoolSize:如果你减少了maxPoolSize,并且当前的线程数超过了新设置的maxPoolSize,多余的线程不会立即终止。只有当它们处于空闲状态并且超过了keepAliveTime时,它们才会被终止。这意味着,在某些情况下,实际的线程数可能会暂时超过maxPoolSize

  3. 并发问题:如果你在多线程环境中动态地修改maxPoolSize,需要确保这种修改不会导致竞争条件或其他线程安全问题。

总的来说,虽然可以动态地修改maxPoolSize,但在做此类修改时需要谨慎,并确保了解其行为和可能的后果。

2.3 根据以下引用,为什么当等待队列满了,才开始创建非核心线程?

从图中可以看出,当提交一个新任务到线程池时,线程池的处理流程如下。
1 默认情况下,创建完线程池后并不会立即创建线程,
而是等到有任务提交时才会创建线程来进行处理。(除非调用prestartCoreThread或prestartAllCoreThreads方法)
2 当线程数小于核心线程数时,每提交一个任务就创建一个线程来执行,即使当前有线程处于空闲状态,直到当前线程数达到核心线程数。
3 当前线程数达到核心线程数时,如果这个时候还提交任务,这些任务会被放到工作队列里,等到线程处理完了手头的任务后,会来工作队列中取任务处理。
4 当前线程数达到核心线程数并且工作队列也满了,如果这个时候还提交任务,则会继续创建线程来处理,直到线程数达到最大线程数。
5 当前线程数达到最大线程数并且队列也满了,如果这个时候还提交任务,则会触发饱和策略。
6 如果某个线程的控线时间超过了keepAliveTime,那么将被标记为可回收的,并且当前线程池的当前大小超过了核心线程数时,这个线程将被终止。

答:这种设计背后的思路是效率和资源的平衡。

  1. 节约资源: 核心线程通常是常驻内存的,这意味着它们不会因为空闲而被回收。为了避免浪费资源,线程池的设计者决定首先使用核心线程来处理任务,直到它们都忙碌为止。这种策略确保了线程池始终使用最小的线程数来处理请求,从而最大化了每个线程的使用。

  2. 工作队列作为缓冲: 当核心线程都在忙碌时,新任务会被放入工作队列。这个队列充当了一个缓冲器,确保在短时间内的请求高峰期间,不会立即创建大量的新线程。这有助于系统吞吐量,并减少了线程创建和销毁的开销。

  3. 应对持续高负载: 如果工作队列满了,说明现有的核心线程和队列的容量都不能应对当前的请求负载。这时,线程池开始创建非核心线程来处理新的请求。这确保了即使在高负载情况下,请求仍然可以得到处理,而不是被拒绝。

  4. 保护系统: 设计中设置了一个最大线程数,其目的是为了确保在极端的情况下,线程池不会创建过多的线程,从而耗尽系统资源。

综上所述,这种策略是为了在高效使用资源和应对高负载之间找到一个平衡。线程的创建和销毁都是有开销的,因此,当有核心线程和工作队列这样的缓冲机制时,可以确保线程池在大多数情况下都能高效地运行。只有在负载真正高到需要更多线程来处理的时候,才会创建额外的线程。

2.4 如果最大线程数达到了阈值,但是等待队列没有,此时新来的任务是放入队列中嘛还是创建新的线程

如果线程池中的当前线程数量已经达到maxPoolSize(即最大线程数),而新的任务又提交到线程池,此时的处理策略是这样的:

  1. 如果工作队列尚未满,新的任务将被放入队列中等待被执行。
  2. 如果工作队列已满,那么线程池会根据其拒绝策略(RejectedExecutionHandler)来处理这个新提交的任务。常见的拒绝策略包括抛出异常、丢弃任务、丢弃队列中的一个旧任务来为新任务腾出空间等。

所以答案是:如果最大线程数已经达到阈值,新来的任务首先尝试放入等待队列。只有当队列也满了的情况下,才会触发拒绝策略。

2.5 如果最大线程数和等待队列都没达到阈值呢

此时说明负载在逐渐降低,

总的来说,当最大线程数和等待队列都没达到阈值时,新提交的任务首先会被尝试分配给一个核心线程。如果核心线程数量已满,那么新任务将会被放入等待队列。

3 线程池的拒绝策略探讨与优化

3.1 线程池有哪些拒绝策略

当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改。(如果某个任务被提交到一个已被关闭的Executor时,也会用到饱和策略)。饱和策略有以下四种,一般使用默认的AbortPolicy。

  • AbortPolicy:中止策略。默认的饱和策略,抛出未检查的RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。
  • DiscardPolicy:抛弃策略。当新提交的任务无法保存到队列中等待执行时,该策略会悄悄抛弃该任务。
  • DiscardOldestPolicy:抛弃最旧的策略。当新提交的任务无法保存到队列中等待执行时,则会抛弃下一个将被执行的任务,然后尝试重新提交新的任务。(如果工作队列是一个优先队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将“抛弃最旧的”策略和优先级队列放在一起使用)。
  • CallerRunsPolicy:调用者运行策略。该策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者(调用线程池执行任务的主线程),从而降低新任务的流程。它不会在线程池的某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务。当线程池的所有线程都被占用,并且工作队列被填满后,下一个任务会在调用execute时在主线程中执行(调用线程池执行任务的主线程)。由于执行任务需要一定时间,因此主线程至少在一段时间内不能提交任务,从而使得工作者线程有时间来处理完正在执行的任务。在这期间,主线程不会调用accept,因此到达的请求将被保存在TCP层的队列中。如果持续过载,那么TCP层将最终发现它的请求队列被填满,因此同样会开始抛弃请求,而tcp层的请求被抛弃后,客户端会因为可靠性机制重传丢失的包,在一定程度上保证一致性。当服务器过载后,这种过载情况会逐渐向外蔓延开来——从线程池到工作队列到应用程序再到TCP层,最终达到客户端,导致服务器在高负载下实现一种平缓的性能降低。

3.2 执行CallerRunsPolicy,任务总是能够最终执行对不对?

是的,CallerRunsPolicy 是一种拒绝策略,但它并不会真的拒绝任务。相反,当线程池和工作队列都满了的时候,它会让调用者自己运行任务。通常,调用者是指调用了 execute() 方法的线程(通常是主线程)。因此,当线程池无法处理任务时,任务总是会在调用者的线程中执行。

这种策略可以确保任务总是最终被执行,并且不会抛弃任何任务。这有助于缓解线程池的压力,但请注意,由于调用者线程会被用于执行任务,这会影响调用者线程的其他工作。当任务的数量持续增加时,调用者线程可能会被阻塞,导致系统的响应能力下降。因此,在选择使用 CallerRunsPolicy 时,需要根据系统的具体需求和场景进行权衡。

3.3 于线程采取DiscardPolicy策略,被抛弃的线程如何处理?

DiscardPolicyThreadPoolExecutor 的一个拒绝策略。当线程池和工作队列都满了的时候,它会直接丢弃新提交的任务。需要注意的是,它并不会抛出任何异常或提供任何通知。

处理 DiscardPolicy 中被抛弃的任务,可以考虑以下几种策略:

  1. 日志记录: 虽然 DiscardPolicy 本身不会提供任何通知,但我们可以通过自定义拒绝策略来记录被丢弃的任务。在自定义拒绝策略中,可以记录被丢弃的任务信息、时间、线程池状态等信息,方便后续的问题分析和排查。

  2. 调整线程池参数: 如果发现线程池经常丢弃任务,可以考虑调整线程池的参数,例如增加线程池的大小、增加工作队列的容量等,从而减少任务被丢弃的情况。

  3. 选择其他拒绝策略: 如果 DiscardPolicy 不适合系统的需求,可以考虑选择其他拒绝策略,例如 AbortPolicy(抛出异常)、CallerRunsPolicy(调用者执行任务)或 DiscardOldestPolicy(丢弃队列中最旧的任务)。

  4. 设计回退机制: 可以在应用层设计回退机制,例如当任务被丢弃时,可以将任务发送到消息队列或持久化到数据库,等待线程池有空闲资源时再重新执行。

  5. 限流或降级: 如果系统的负载过高,导致线程池经常丢弃任务,可以考虑采取限流或降级的策略,减少系统的负载,保证核心功能的正常运行。

总之,被丢弃的任务需要根据系统的需求和场景进行合理处理,避免影响系统的稳定性和用户体验。

4 线程基础

4.1 java线程创建方式

在 Java 中,创建线程的方式有多种,主要包括以下几种方式:

  1. 继承 Thread 类

    • 创建一个新类并继承 java.lang.Thread 类。
    • 重写 run() 方法,定义线程执行的逻辑。
    • 创建该类的实例,并调用 start() 方法启动线程。
    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Thread is running");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            MyThread myThread = new MyThread();
            myThread.start();
        }
    }
    
  2. 实现 Runnable 接口

    • 创建一个新类并实现 java.lang.Runnable 接口。
    • 重写 run() 方法,定义线程执行的逻辑。
    • 创建 Thread 类的实例,将 Runnable 实现类作为构造函数的参数传入,并调用 start() 方法启动线程。
    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("Runnable is running");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Thread thread = new Thread(new MyRunnable());
            thread.start();
        }
    }
    
  3. 实现 Callable 接口

    • 创建一个新类并实现 java.util.concurrent.Callable 接口。
    • 重写 call() 方法,定义线程执行的逻辑,并返回结果。
    • 使用 FutureTask 类包装 Callable 对象,并将其传递给 Thread 类的构造函数,然后调用 start() 方法启动线程。
    class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() {
            return 42;
        }
    }
    
    public class Main {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
            Thread thread = new Thread(futureTask);
            thread.start();
            System.out.println("Callable result: " + futureTask.get());
        }
    }
    
  4. 使用线程池

    • 使用 java.util.concurrent.Executors 类的工厂方法创建线程池。
    • 将实现了 RunnableCallable 接口的任务提交给线程池执行。
    public class Main {
        public static void main(String[] args) {
            ExecutorService executorService = Executors.newFixedThreadPool(2);
            executorService.submit(() -> System.out.println("Runnable in thread pool is running"));
            executorService.shutdown();
        }
    }
    

以上四种方式中,使用线程池的方式通常是首选,因为它可以有效地管理和重用线程资源,提高性能,并降低线程创建和销毁的开销。

4.2 FutureTaskCompletableFuture 都是 Java 并发编程中用来表示异步计算结果的类,但它们在使用和功能上有一些区别。

  1. 使用方式的区别

    • FutureTask 是一个可以取消的异步计算任务。它通常用于包装 Callable 对象,然后通过 Thread 来执行。
    • CompletableFuture 是 Java 8 引入的一个新的类,它不仅可以包装异步计算任务,还提供了更多的功能,例如组合多个异步任务、应对异常等。
  2. 功能上的区别

    • FutureTask 只提供了基本的异步任务管理功能(启动、取消、获取结果等)。
    • CompletableFuture 提供了丰富的功能来组合、处理和操作异步任务。例如,你可以使用 thenApplythenComposethenCombine 等方法来组合多个异步任务。还可以使用 handleexceptionally 等方法来处理异常。
  3. 阻塞与非阻塞

    • 使用 FutureTaskget() 方法获取结果时,如果异步任务还未完成,该方法会阻塞当前线程,直到异步任务完成。
    • CompletableFuture 提供了非阻塞的方式来处理异步任务的结果,例如使用 thenAccept 方法来处理结果。

总之,CompletableFuture 是一个更加强大和灵活的工具,它提供了更多的功能来处理和组合异步任务。而 FutureTask 只是提供了基本的异步任务管理功能。如果你需要进行更复杂的异步任务操作和组合,CompletableFuture 会是一个更好的选择。

4.3 线程池中的poll(),take()动作解析

建议看看:Java线程池应该如何使用?

4.3.1 对于线程池的BlockingQueue队列,或者对象的等待队列而言,当使用take()方法时,阻塞队列中没有消息内容,核心线程会阻塞,那此时当队列中有内容时,线程怎么知道并且去再take呢?

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class PrintAB {
    public static void main(String[] args) {
        BlockingQueue<Character> queueA = new ArrayBlockingQueue<>(1);
        BlockingQueue<Character> queueB = new ArrayBlockingQueue<>(1);

        Thread threadA = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    System.out.print("A ");
                    queueB.put('B');
                    queueA.take();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread threadB = new Thread(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    queueB.take();
                    System.out.print("B ");
                    queueA.put('A');
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        threadA.start();
        threadB.start();
    }
}

当阻塞队列中没有内容时,调用take()方法的线程会被阻塞。这是通过Java中的Object.wait()Object.notify()实现的。

内部实现中,当线程调用take()方法,且队列为空时,线程会调用wait()方法阻塞自身。然后,当其他线程调用put()方法往队列中添加元素时,它会调用notify()方法来唤醒因wait()而被阻塞的线程。

此时被阻塞的线程会被唤醒,然后重新尝试获取队列中的内容。这样,线程就能取到新加入的元素。

总结起来,是通过Java对象的wait()notify()机制来实现线程在队列为空时的阻塞,以及在新元素加入时的唤醒。这样的机制确保了线程能够在队列中有新内容时及时取到数据。

4.3.2 这里的wait()和notify()方法应该来自BlockingQueue吧

对的,BlockingQueue内部使用了wait()notify()方法来实现线程的阻塞和唤醒。

4.3.3 如果有多个线程执行了wait()方法,依次被放入了等待队列,当另一个线程执行notifyAll()方法时,会唤醒所有线程竞争,如果调用notify()只会唤醒等待队列头部的线程嘛?如果我想要唤醒等待队列中指定位置的线程呢,该怎么办

当多个线程执行了wait()方法后,它们会被放入等待队列。当另一个线程执行notifyAll()方法时,会唤醒所有等待的线程。这些线程会竞争进入同步块,但只有一个线程能成功获取锁并继续执行。

当调用notify()方法时,只会唤醒等待队列中的一个线程。你不能指定唤醒哪一个线程,因为wait()notify()机制是基于Java对象的内置锁和监视器实现的,它们没有提供选择唤醒特定线程的能力。

如果你需要更加灵活的线程控制和通信,可以使用java.util.concurrent包中的其他工具,比如ReentrantLockCondition。使用Condition可以创建多个条件变量,从而能更精确地控制哪些线程被唤醒。

4.3.4 如果调用notify()只会唤醒等待队列头部的线程,还是不一定头部

调用notify()方法会唤醒等待队列中的任意一个线程。通常情况下,它会唤醒等待队列中最早进入阻塞状态的线程(即队列头部的线程),但这取决于JVM的具体实现和调度策略。因此,在实践中,我们不能假设它总是唤醒头部线程。

记住,notify()wait()机制的目的是简化多线程间的协作和通信,而不是提供精确的线程控制。如果需要更精确的控制,可以考虑使用ReentrantLockCondition等高级并发工具。

4.3.5 线程池中的poll()方法和带时间戳的poll()方法的区别

线程池中的poll()方法和带时间戳的poll(long timeout, TimeUnit unit)方法的主要区别在于等待时间和行为。

  1. poll()方法:

    • 这个方法会立即返回,如果队列中有可用元素,它会立即返回该元素。如果队列为空,它会立即返回null, 然后就会被销毁。这个方法不会等待元素变得可用。
  2. poll(long timeout, TimeUnit unit)方法:

    • 这个方法会等待指定的时间,以便从队列中检索并移除一个元素。如果在指定的等待时间内,队列中有元素变得可用,它会返回该元素。如果等待时间到达而队列仍然为空,它会返回null,在这个时间被销毁。这个方法允许线程等待一段时间,直到有一个元素可以从队列中取出。

总的来说,poll()方法是非阻塞的,它会立即返回;而带时间戳的poll()方法是有可能阻塞的,它会等待一段时间。在使用这两个方法时,要根据具体的使用场景和需求来选择合适的方法。

4.3.6 我看你简历上写的项目中用到了线程池,你知道线程池是怎样实现复用线程的?

线程池复用线程的逻辑很简单,就是在线程启动后,通过while死循环,不断从阻塞队列中拉取任务,从而达到了复用线程的目的。其实就是在问poll和take方法

4.3.7 我们都知道线程池会回收超过空闲时间的线程,那么线程池是怎么统计线程的空闲时间的?

我的答案:可能是有个监控线程在后台不停的统计每个线程的空闲时间,看到线程的空闲时间超过阈值的时候,就回收掉。
官方答案:阻塞队列(BlockingQueue)提供了一个 poll(time, unit) 方法用来拉取数据, 作用就是: 当队列为空时,会阻塞指定时间,然后返回null。线程池就是就是利用阻塞队列的这个方法,如果在指定时间内拉取不到任务,就表示该线程的存活时间已经超过阈值了,就要被回收了。也就是说线程自己复用线程自己计时并且到期自我销毁

4.3.8 线程池如何设置异常捕获的逻辑

如果线程池中的线程抛出了一个未捕获的异常,并且没有被try-catch语句块捕获,那么以下几件事情会发生:

  1. 异常会导致当前线程的执行终止,即该线程不再执行任务。

  2. 如果该线程是线程池中的一个工作线程,线程池会检测到该线程的终止,并可能会创建一个新的线程来替代它,从而维护线程池中的线程数量。

  3. 如果未设置未捕获异常处理器(UncaughtExceptionHandler),那么异常的堆栈信息会被打印到System.err

  4. 如果你设置了未捕获异常处理器,那么该处理器将会被调用。你可以在该处理器中进行错误记录、资源清理等操作。

注意:如果你在RunnableCallable任务中捕获并处理了异常,那么线程池不会知道这些异常,因此上述情况不会发生。此外,对于FutureTaskExecutorService.submit方法提交的任务,任何从任务中抛出的异常都会被捕获并在调用Future.get方法时通过ExecutionException重新抛出。

总之,为了避免由于未捕获的异常导致的线程终止和可能的资源泄露,建议在任务代码中适当处理异常。

4.3.9 这里的如果未设置未捕获异常处理器(UncaughtExceptionHandler),那么异常的堆栈信息会被打印到System.err。java中时如何实现这种UncaughtExceptionHandler机制的

Java中的UncaughtExceptionHandler机制是通过Thread类的setUncaughtExceptionHandler方法来设置的。当线程因为未捕获的异常而终止时,JVM会查询该线程是否设置了UncaughtExceptionHandler,如果设置了,则会调用它的uncaughtException方法来处理这个异常。

以下是使用UncaughtExceptionHandler的一个简单示例:

public class Main {

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            throw new RuntimeException("Test Exception");
        });

        thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
            @Override
            public void uncaughtException(Thread t, Throwable e) {
                System.out.println("Caught exception in thread " + t.getName() + ": " + e.getMessage());
            }
        });

        thread.start();
    }
}

在这个示例中,我们创建了一个线程,并在该线程的运行过程中抛出了一个RuntimeException。然后,我们设置了一个UncaughtExceptionHandler,当线程因为未捕获的异常而终止时,uncaughtException方法会被调用,我们可以在这里处理异常。

在没有设置UncaughtExceptionHandler的情况下,未捕获的异常会导致线程终止,并将异常的堆栈信息打印到System.err。这是通过ThreadGroup类的uncaughtException方法来实现的。当线程终止时,JVM会调用该线程的ThreadGroup对象的uncaughtException方法,并将异常的堆栈信息打印到System.err

注意,如果你没有为特定线程设置UncaughtExceptionHandler,线程组的默认处理器会被调用。你也可以为所有线程设置一个默认的未捕获异常处理器,通过调用Thread.setDefaultUncaughtExceptionHandler方法来实现。

4.3.10 所以线程发生异常时,发生异常的线程本身能够感受到,这个时候采用回调函数是嘛?

是的,当线程内发生未捕获的异常时,JVM会检查该线程是否设置了UncaughtExceptionHandler。如果设置了,JVM会调用该处理器的uncaughtException方法,将异常对象和发生异常的线程作为参数传递给这个方法。这是一个回调函数的形式,允许你自定义如何处理未捕获的异常。

如果该线程没有设置UncaughtExceptionHandler,JVM会进一步查找该线程所属的ThreadGroup是否设置了异常处理器。如果设置了,将调用ThreadGroupuncaughtException方法。如果没有设置,JVM将调用Thread类的getDefaultUncaughtExceptionHandler方法,查找是否设置了默认的异常处理器。

如果以上所有步骤都没有找到合适的异常处理器,JVM将会将异常信息打印到System.err

4.3.11 线程执行异常了,会不会造成数据不一致?

线程执行异常可能会造成数据不一致,具体取决于异常发生的位置和线程中数据操作的情况。以下是一些常见的导致数据不一致的情况:

  1. 数据操作没有完成:如果线程在修改数据时发生异常并终止,那么数据可能会被留在一个不一致的状态。例如,如果线程在一个事务中更新了一部分数据但在完成事务之前发生异常,那么可能会造成数据不一致。

  2. 资源锁未释放:如果线程在持有资源锁时发生异常,可能会导致锁未被释放,进而阻塞其他线程,这也可能导致数据不一致。

  3. 未正确回滚事务:如果线程在执行数据库事务中发生异常,但未能正确回滚事务,也可能导致数据不一致。

为了避免数据不一致的问题,建议采取以下措施:

  1. 异常处理:在代码中添加适当的异常处理逻辑,确保异常情况下能够恢复到一个一致的状态。

  2. 使用事务:在数据库操作中使用事务,确保数据的一致性。事务可以确保一系列操作要么全部成功,要么全部失败。

  3. 使用锁:在访问共享资源时使用适当的锁机制,确保资源在被多个线程访问时能保持一致性。

  4. 使用原子操作:在多线程环境中使用原子操作来更新数据,确保数据的一致性。例如,Java的java.util.concurrent.atomic包中提供了一系列原子操作类,如AtomicIntegerAtomicLong等。

总之,线程执行异常可能会导致数据不一致,但通过适当的设计和编程实践,可以避免这种情况。

4.4 jdk的线程池放满核心线程后,为什么后面的任务放到队列中,而不是创建线程处理直至数量达到了最大线程数的阈值?

这个策略的设计是为了平衡线程的创建成本和系统资源消耗。如果线程池一直创建新线程,可能会导致系统资源耗尽(比如cpu,内存空间),而线程的创建和销毁也会带来额外的开销。通过将任务放入队列,可以有效地控制线程数量,避免资源浪费。

在实际应用中,你可以根据具体的需求和系统资源来配置线程池的参数,以达到最佳性能。

4.5 为什么队列满了之后,还要继续创建非核心线程?难道更多线程的创建,有利于更快执行嘛?那这个时候线程的竞争问题,创建和销毁带来的资源损耗呢?

  1. 更快的执行:在某些情况下,创建更多的线程可以提高任务执行的速度,尤其是当任务是 CPU 密集型的,而不是 IO 密集型的。创建更多线程允许多个任务同时运行,从而提高了任务的并行性,可以更快地完成任务队列中的工作。
  2. 任务等待时间:如果工作队列中有大量等待执行的任务,创建额外的线程可以减少任务在队列中的等待时间,提高响应性。这对于某些应用程序非常重要,例如 Web 服务器,需要快速响应客户端请求。
  3. 尽可能少拒绝请求:如果队列满了之后不创建线程,那么线程池会立即采取拒绝策略,这个时候对于还有冗余能力的服务器来说是不应该的,所以尽可能创建非核心线程去处理队列中的任务,尽可能少的拒绝请求。

你可能感兴趣的:(java,线程池)