Java并发基础2
Java并发基础2
16. 为什么线程通信的方法 wait(), notify() 和 notifyAll() 被定义在 Object 类里?
回答:
这是因为 Java 的锁是对象级别的,而不是线程级别的。线程通信方法之所以定义在 Object 类中,是为了让任何对象都具备作为同步监视器的能力,从而允许线程在对象上等待和唤醒。
分析:
在 Java 中,每个对象都天然拥有一把锁(也称为监视器锁),线程通过 synchronized 关键字获取该对象锁来实现同步。线程间通信本质上是通过锁对象来协调执行顺序的,因此等待与通知的行为也必须作用于这个"共享对象"。若将 wait()、notify() 设计为 Thread 的方法,会打破 Java 的同步模型:线程的等待和唤醒依赖于共享资源的状态变化,而不是线程自身状态。一个线程可以持有多个对象的锁,如果通信方法设计在线程级别,那么在多个锁的情况下将难以判断具体关联的对象,增加了管理和使用的复杂性。将这三个方法放在 Object 类中,则任何对象都可以作为锁使用,使得同步与通信语义统一,简化了设计与使用。这种设计遵循了面向对象的封装思想,也是 Java 并发模型中一个非常精妙的机制。
17. 为什么 wait(), notify() 和 notifyAll() 必须在同步方法或者同步块中被调用?
回答:
因为这些方法依赖于线程已经持有的对象锁,否则会抛出 IllegalMonitorStateException。只有在同步方法或同步块中,线程才能获得对象锁,才能安全调用这些通信方法。
分析:wait()、notify() 和 notifyAll() 是 Java 提供的线程间通信手段,用于实现协调机制。而协调的前提是:调用这些方法的线程必须已经持有该对象的监视器锁。也就是说,线程必须通过 synchronized 成功进入了该对象的同步代码块或同步方法中,才能安全地执行这些操作。否则,Java 会抛出 IllegalMonitorStateException 异常。这种设计并非强制限制,而是出于并发一致性的保障。若不具备锁的上下文就调用 wait(),线程将陷入等待但无锁释放,其他线程也无法唤醒它;若随意调用 notify(),可能导致未在等待队列中的线程被意外唤醒,破坏线程协作逻辑。因此 Java 在语言层级进行了限制,必须在 synchronized 控制块中使用这三种方法,确保同步与通信的语义一致。
18. 线程的 sleep() 方法和 yield() 方法有什么区别?
回答:sleep() 和 yield() 都可以使当前线程暂停执行,但前者会进入阻塞状态并有明确的暂停时长,后者只是表示"愿意"让出 CPU 执行权,是否让出由操作系统决定。
分析:sleep() 是一种强制性的暂停方式,当线程调用 Thread.sleep(ms) 后,会进入 TIMED_WAITING 状态,也就是阻塞状态,暂停指定毫秒后才会恢复运行,并且不会主动释放已持有的锁。它通常用于定时等待、模拟延迟等场景。相比之下,yield() 是一种"软性"调度建议,它使线程进入就绪(RUNNABLE)状态,让出当前时间片给同优先级或更高优先级的其他线程。但 yield() 是否真的让出,取决于底层调度器的实现,因此行为不确定,具有较差的可移植性。此外,sleep() 会抛出 InterruptedException,而 yield() 不会抛出任何异常。从实际使用角度看,sleep() 更具可控性,而 yield() 仅在特定性能调试场景中使用较多,生产代码中通常不建议依赖它来控制线程调度。
19. 如何停止一个正在运行的线程?
回答:
推荐通过设置"退出标志位"的方式来优雅地终止线程,其它方式如 stop() 已被废弃,interrupt() 则用于配合响应中断的线程逻辑使用。
分析:
在 Java 中终止一个线程并不能直接"杀死"它,而是要通过合理设计线程逻辑让其"自行退出"。最常见的做法是在线程内部定义一个 volatile 的布尔型标志位,例如 running,线程在运行过程中不断轮询该标志来决定是否退出。这种方式安全、可控,能确保线程在清理资源后退出,适用于多数业务场景。虽然 Java 也提供了 Thread.stop() 方法,但它会强制中断线程执行并立即释放所有资源,可能导致数据不一致或破坏程序状态,因此已被弃用。而 Thread.interrupt() 则是一种"温和"的中断方式,它仅仅设置线程的中断状态,具体是否响应取决于线程内部逻辑是否处理了中断,例如通过 InterruptedException 进行跳出。综合而言,推荐优先使用"标志位 + 线程内部判断"的方式来安全终止线程,interrupt() 适合作为辅助信号通知机制。
20. Java 中 interrupted 和 isInterrupted 方法的区别?
回答:interrupted() 是静态方法,用于检测当前线程是否被中断,并清除中断状态;isInterrupted() 是实例方法,用于判断指定线程是否中断,但不会清除状态。
分析:
这两个方法都是用来检测线程中断状态的,但使用场景和行为存在显著差异。Thread.interrupted() 是一个静态方法,作用于当前线程,调用它时会返回中断状态,并立即将该状态重置为 false。这意味着它是"一次性读取",只能在特定语义中使用,常用于循环中判断并清除中断信号。而 isInterrupted() 是实例方法,可以检查任意线程是否被中断,并且不会清除中断标志,因此适用于长期观察线程状态的场景。需要注意的是,仅仅设置中断状态并不会真正中止线程,除非线程主动响应,比如在执行 sleep()、wait() 等方法时会触发 InterruptedException,此时才能真正退出逻辑流程。正确理解和区分这两个方法,有助于编写更健壮的中断控制逻辑,避免遗漏中断处理或误判线程状态。
21. 什么是阻塞式方法?
回答:
阻塞式方法是指在调用过程中线程会被挂起,直到结果返回之前无法继续执行后续操作。常见如 accept()、read() 等 I/O 操作。
分析:
阻塞在并发编程中是一个常见但重要的概念。所谓阻塞式方法,是指调用该方法后线程会停止执行当前任务,直到该方法返回结果。例如在服务端使用 ServerSocket.accept() 方法时,线程会一直等待客户端连接请求,在连接建立前不会向下执行。阻塞不仅会暂停当前线程,也会占用相关资源,如网络、内存等,这在高并发场景中容易成为性能瓶颈。与之相对的概念是非阻塞和异步操作,它们允许方法在尚未完成时立即返回,避免线程等待,提升资源利用率。因此,在构建并发系统时,需要根据实际需求选择是否使用阻塞方式,合理配合线程池、事件模型等机制,以提升系统响应能力和扩展性。
22. Java 中你怎样唤醒一个阻塞的线程?
回答:
可以通过调用对象的 notify() 或 notifyAll() 方法唤醒阻塞在该对象上的线程。配合 wait() 使用时,线程需要先获取该对象的锁才能被唤醒继续执行。
分析:
Java 使用对象的监视器锁来实现线程间的等待与唤醒机制。当一个线程在执行 wait() 方法时,它会释放当前对象的锁并进入等待队列,处于 WAITING 状态,直到被 notify() 或 notifyAll() 唤醒。此时被唤醒的线程并不会立即执行,它需要重新竞争该对象的锁,只有在获得锁之后,才能从 wait() 语句处继续向下执行。因此唤醒并非立即恢复运行,而是进入锁池等待重新调度。调用这些方法的前提是当前线程已经持有该对象的锁,否则会抛出 IllegalMonitorStateException。这一机制确保了线程间通信的安全性和有序性,避免出现状态不一致的问题。在构建线程协调逻辑时,正确使用同步块、锁对象和通信方法是保障线程安全协作的基础。
23. notify() 和 notifyAll() 有什么区别?
回答:notify() 只唤醒一个在该对象等待队列中的线程,而 notifyAll() 会唤醒所有在该对象上等待的线程,二者都需要在持有对象锁的前提下调用。
分析:
当多个线程因为调用 wait() 而阻塞在同一个对象上时,这些线程被加入该对象的等待队列中,不再参与锁竞争。调用 notify() 方法会从等待队列中随机唤醒一个线程,该线程随后进入锁池,等待重新竞争锁资源。而 notifyAll() 会将所有等待线程全部移动到锁池中,参与锁的竞争。具体哪一个线程最终获得锁并恢复执行,由 JVM 的调度机制决定。选择使用 notify() 还是 notifyAll() 取决于业务场景:如果能够确保只需要一个线程被唤醒且能处理当前状态,使用 notify() 更高效;但如果无法确定状态是否适配,或需要多个线程重新评估条件,使用 notifyAll() 更保险,避免出现线程永远等待的问题。在实际开发中,推荐优先使用 notifyAll(),配合条件判断更能保障逻辑正确性。
24. Java 如何实现多线程之间的通讯和协作?
回答:
Java 提供了多种线程通信机制,其中最核心的是通过共享变量配合 wait()/notify() 或 Condition 的 await()/signal() 实现线程协作。典型场景是生产者-消费者模型。
分析:
线程间的通信和协作通常发生在多个线程需要对共享资源进行有序访问时,Java 提供的通信机制基于"等待-通知"模式来实现。例如在经典的生产者-消费者模型中,当队列满时,生产者调用 wait() 释放锁并进入等待;消费者消费后调用 notify() 唤醒生产者,反之亦然。若使用 synchronized 关键字进行同步,可直接调用 Object.wait() 和 notify();若采用 ReentrantLock,则可通过 Condition.await() 与 signal() 实现更灵活的控制。除了基于锁的协调方式,Java 还提供了如 PipedInputStream/PipedOutputStream 等基于管道的直接数据通信方式。无论是哪种机制,线程间通信的关键在于保证数据共享状态的可见性与同步性,同时防止死锁、饥饿等并发陷阱。合理设计线程通信策略是高并发系统架构的核心能力之一。
25. 同步方法和同步块,哪个是更好的选择?
回答:
同步块通常是更优的选择,因为它允许我们精确控制锁的粒度,只在必要代码段加锁,避免资源的过度竞争与性能浪费。
分析:synchronized 关键字在 Java 中既可以用于修饰方法,也可以用于修饰代码块。两者都能实现线程同步,但其作用范围截然不同。同步块则可以灵活选择锁定的代码范围以及锁对象。因此,同步块提供了更精细的控制能力,能够让我们将锁的范围缩小到真正需要同步的代码上,从而减少竞争,提高程序的并发性能。此外,同步块也更符合"开放调用原则" —— 尽量在非锁区域执行耗时操作,降低死锁风险。在实际项目中,合理使用同步块,不仅有助于提升系统吞吐量,也能增强程序的可维护性。一个通用的经验是:同步范围越小越好。
26. 什么是线程同步和线程互斥?有哪几种实现方式?
回答:
线程同步是确保多线程对共享资源的有序访问,而线程互斥是一种同步方式,用于避免多个线程同时访问同一资源导致的冲突。实现手段包括 synchronized、Lock、volatile、原子变量等。
分析:
在并发环境下,线程往往需要访问同一份数据,如果不加以控制,容易产生数据竞争和不一致的问题。线程同步的核心目标,就是保证这些访问操作是原子的、有序的、线程间可见的。而线程互斥可以理解为一种"加锁"的同步机制,用于确保某一时刻只有一个线程能够执行某段关键代码。Java 中的 synchronized 是最基础的同步工具,可以用来修饰方法或代码块,实现互斥访问;ReentrantLock 则提供了更丰富的控制能力,如中断响应、尝试锁定、公平策略等;volatile 虽不具备互斥功能,但能保证变量对所有线程的可见性;而 AtomicInteger、LongAdder 等原子类提供了在用户态下无锁的高效并发更新机制。此外,还可通过信号量(Semaphore)、倒计时器(CountDownLatch)、屏障(CyclicBarrier)等实现更复杂的同步模式。选择哪种方式,需要根据任务的同步粒度、性能需求和编程复杂度权衡取舍。
27. 在监视器(Monitor)内部,是如何做线程同步的?程序应该做哪种级别的同步?
回答:
Java 的监视器机制通过对象锁保证线程同步,synchronized 关键字会将代码或方法绑定到对象的监视器上,从而确保同一时刻只有一个线程能够执行被同步的代码。
分析:
在 Java 虚拟机中,每个对象都关联着一个监视器(Monitor),当线程进入 synchronized 修饰的方法或代码块时,JVM 会尝试获取该对象的监视器锁。成功获取后,线程才能进入临界区执行;否则将进入阻塞状态,直到锁可用。这个过程由底层的对象头与监视器锁实现同步控制,确保了线程对共享资源的互斥访问。监视器本质上是 JVM 内部用于管理线程同步的数据结构,通过它可以实现线程间的竞争、等待与唤醒机制。除了隐式的 synchronized,Java 还提供了显式的同步工具如 Lock 接口,给予开发者更大的控制权。至于同步的"级别",原则上应尽量缩小同步范围,只在必要区域加锁,避免过度串行化导致性能下降,同时也减少死锁风险。在需要高可控性或复杂协作逻辑的场景下,显式锁往往是更灵活的选择。
28. 在 Java 程序中怎么保证多线程的运行安全?
回答:
保障多线程运行安全的核心在于控制对共享资源的访问,可以通过 synchronized、显式 Lock、原子类等机制来实现同步控制与状态可见性。
分析:
Java 提供了多种方式来实现线程安全。最常见的是使用 synchronized,通过在方法或代码块上加锁,确保同一时间只有一个线程能访问关键代码段。此外,Lock 接口提供了更灵活的控制能力,比如 ReentrantLock 允许中断锁等待、公平锁设置、可轮询尝试等。在高并发场景中,java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)则通过 CAS(Compare-And-Swap)机制实现了无锁同步,性能更优。还有如并发容器(ConcurrentHashMap)或线程安全工具类(如 CopyOnWriteArrayList)也能简化线程安全处理。选择哪种机制,应基于访问模式、资源共享复杂度与性能要求综合考虑,优先使用高层抽象,避免过度加锁造成性能瓶颈。
29. 你对线程优先级的理解是什么?
回答:
线程优先级是调度线程的一个提示,它表示线程的重要程度,但不能保证优先级高的线程就一定会先执行。实际调度依赖于操作系统的实现。
分析:
每个 Java 线程都有一个优先级,范围从 1(最低)到 10(最高),默认值为 5。可以通过 setPriority(int) 方法设置线程优先级。理论上,线程调度器会倾向于选择高优先级的线程先执行,但 Java 的线程调度依赖底层操作系统,不同平台对优先级的处理方式不同,有些可能完全忽略优先级设置。在大多数实际项目中,线程优先级的效果有限,不推荐依赖它来实现业务层的调度逻辑。此外,优先级的滥用可能会造成线程"饥饿",即低优先级线程长时间得不到执行机会。因此更稳妥的做法是使用线程池、任务队列等方式控制调度顺序,而非依赖操作系统层的线程调度策略。
30. 线程类的构造方法、静态块是被哪个线程调用的?
回答:
线程类的构造方法与静态代码块是由创建该线程的线程调用的,而不是线程自己。线程自身只会执行其 run 方法。
分析:
这个问题往往用于考察面试者对线程创建过程的理解。当我们在某个线程中通过 new Thread() 创建新线程实例时,该构造方法由当前线程执行,而非新线程本身。同理,类加载过程中执行的静态块也是由加载类的线程完成的。新线程启动后,会调用其 run() 方法,这部分才是真正由新线程执行的代码。例如,在主线程中创建 ThreadA,那么 ThreadA 的构造方法和类的静态块由主线程执行,而当我们调用 ThreadA.start() 后,run() 方法才会在 ThreadA 线程中运行。这个区分对于分析类初始化、资源分配顺序和线程行为追踪非常关键,是理解线程生命周期的基础。
31. Java 中怎么获取一份线程 dump 文件?你如何在 Java 中获取线程堆栈?
回答:
线程 dump 是用来分析线程状态的重要工具,可以通过 jstack 命令或特定信号触发生成。它展示了每个线程的调用栈,有助于定位死锁、阻塞等问题。
分析:
线程 dump 是 Java 应用运行时所有线程的快照,记录了每个线程的状态(如 RUNNABLE、WAITING、BLOCKED)以及栈帧信息。在 Linux 下,最常用的方法是使用 jstack -l <pid> 命令,其中 pid 是 Java 进程号。这会输出线程状态、锁持有情况、堆栈调用等详细信息,可用于排查死锁、卡顿和性能瓶颈。在 Windows 环境下,可以使用 Ctrl + Break 来触发 dump 输出,具体位置可能是控制台或日志文件。对于运行在容器中的程序,还可以通过发送 kill -3 信号(不会终止进程)来让 JVM 输出 dump 内容到标准错误流。掌握线程 dump 获取方法并能解读其中信息,是一名资深 Java 开发者分析并发问题时的重要技能。
32. 一个线程运行时发生异常会怎样?
回答:
如果线程运行过程中抛出未捕获异常,该线程会终止执行。可以通过设置 UncaughtExceptionHandler 来捕获和处理这些异常,避免线程意外退出带来的影响。
分析:
在多线程环境中,某个线程如果在执行过程中抛出未被捕获的异常(例如数组越界、空指针等),JVM 默认会终止该线程的运行,但不会影响其他线程。为避免线程异常退出导致服务异常或数据不一致,Java 提供了 Thread.UncaughtExceptionHandler 接口来集中处理未捕获的异常。我们可以通过 Thread.setUncaughtExceptionHandler() 为每个线程设置独立的异常处理器,或者使用 Thread.setDefaultUncaughtExceptionHandler() 设置全局处理器。一旦发生异常,JVM 会调用 handler 的 uncaughtException(Thread t, Throwable e) 方法,并传入异常信息和出错线程。这种机制在构建稳定可靠的并发程序时非常有用,特别适合记录错误日志、报警或进行线程恢复重启等操作。
33. Java 线程数过多会造成什么异常?
回答:
线程数过多会导致系统资源耗尽,造成频繁的上下文切换,最终可能抛出 OutOfMemoryError: unable to create new native thread 异常,甚至影响 JVM 的整体稳定性。
分析:
Java 中每个线程都会占用一定的内存资源,主要包括线程栈、线程对象和线程调度开销。当线程数激增时,不仅会增加内存压力,还会导致线程间频繁上下文切换,严重消耗 CPU 性能。如果系统无法为新线程分配足够的内存或调度资源,就会触发 OutOfMemoryError 异常。这种情况通常发生于未控制线程创建数量,例如无界线程池或递归创建线程的场景。此外,不同操作系统对线程数量的上限存在差异,与 JVM 参数如 -Xss(线程栈大小)、进程内存限制、文件描述符数量等密切相关。为防止线程过多问题,建议使用线程池管理线程生命周期,限制最大线程数,并监控线程活动状况。保持线程数量在可控范围内,是构建高性能、高稳定性系统的基本要求。
34. 多线程有哪些常见使用方式?
回答:
常见使用方式包括继承 Thread 类、实现 Runnable 接口、实现 Callable 接口配合 Future 使用,实际开发中多通过线程池来统一管理线程资源。
分析:
Java 提供了多种方式实现多线程,最基础的是继承 Thread 类并重写 run() 方法,但由于 Java 单继承限制,这种方式在实际项目中使用较少。更常用的是实现 Runnable 接口,将任务逻辑写在 run() 方法中,通过 new Thread(new MyRunnable()).start() 启动线程,这种方式更加灵活。若任务需要返回结果或抛出异常,则可实现 Callable 接口,配合 FutureTask 使用,能够在任务执行完后异步获取结果。此外,Java 提供了线程池机制(如 ThreadPoolExecutor)来统一管理线程,避免频繁创建与销毁所带来的开销。无论使用哪种方式,都建议通过线程池进行线程管理,确保资源可控、行为可追踪。
35.多线程的常用方法 按回答和分析的格式给我输出下这道题,语言要比较自然流畅
回答:
多线程的常用方法包括线程生命周期控制(如start()启动线程、sleep()休眠、join()等待)、线程同步(如wait()/notify()实现协作、synchronized加锁)、线程状态管理(如getState()获取状态、interrupt()中断)以及工具方法(如Thread.currentThread()获取当前线程),需注意废弃方法(如stop())的避免以及wait()/notify()必须搭配synchronized使用,确保线程安全和高效协作。
分析:
| 方法/关键字 | 作用 | 使用场景 | 注意事项 | 底层原理 |
|---|---|---|---|---|
start() | 启动线程,调用run()方法 | 创建并执行新线程 | 多次调用会抛IllegalThreadStateException | JVM调用本地方法创建OS线程 |
run() | 定义线程执行的任务 | 重写以实现线程逻辑 | 直接调用相当于普通方法,不会异步执行 | 单纯的方法调用,无特殊机制 |
sleep(long) | 线程休眠指定毫秒(不释放锁) | 模拟耗时操作或定时任务 | 需处理InterruptedException | 调用OS的线程调度器暂停线程 |
join() | 等待目标线程终止 | 线程顺序执行(如A需等B完成) | 可能阻塞当前线程 | 基于wait()机制实现 |
interrupt() | 中断线程(设置标志位) | 优雅终止线程 | 需配合isInterrupted()或异常检测 | 设置中断标志,不强制停止线程 |
wait() | 释放锁并进入等待状态 | 线程间协作(生产者-消费者) | 必须在synchronized块中使用 | 依赖对象监视器(Monitor) |
notify() | 随机唤醒一个等待线程 | 多线程通知机制 | 需持有相同对象锁 | 通过Monitor通知等待队列 |
yield() | 提示让出CPU(实际效果不确定) | 避免线程过度占用CPU | 不保证其他线程一定能运行 | 调用OS线程调度器,优先级调整 |
synchronized | 保证代码块/方法同步执行 | 解决竞态条件(如计数器++) | 锁对象需唯一,避免死锁 | 基于JVM的Monitor锁 |
volatile | 保证变量可见性,禁止指令重排序 | 状态标志位(如while(!flag)) | 不保证原子性(如i++仍需同步) | 插入内存屏障(Memory Barrier) |
36. 介绍一下 ThreadLocal
回答:
ThreadLocal 用于为每个线程提供独立的变量副本,实现线程级的隔离性。每个线程通过 get/set 方法访问属于自己的值,底层由线程维护 ThreadLocalMap 实现。
分析:
ThreadLocal 是实现线程隔离的一种轻量机制,它并不直接存储变量值,而是由每个线程维护一个 ThreadLocalMap 实例,使用 ThreadLocal 对象作为 key 存储线程私有变量。这种设计使得多个线程访问同一个 ThreadLocal 实例时,实际读写的是自己线程内部的数据副本,互不干扰,常用于事务上下文、用户会话、请求追踪等场景。Thread 类内部维护了 threadLocals 和 inheritableThreadLocals 两个变量,分别用于普通副本和父子线程继承副本。当线程第一次调用 ThreadLocal.get() 或 set() 时,会初始化对应的 Map,并存入 <ThreadLocal, value> 键值对。ThreadLocal 并不保证数据隔离的"安全性",但通过结构上天然避免了多线程竞争,是一种简洁高效的线程绑定方案。
37. ThreadLocal 的内存泄漏问题你了解吗?
回答:
ThreadLocal 本身采用弱引用作为 key,当 key 被回收但 value 未被清理时,容易造成内存泄漏。因此使用 ThreadLocal 后应主动调用 remove() 清除数据。
分析:
ThreadLocal 使用弱引用作为其在 ThreadLocalMap 中的 key,目的是为了避免内存泄漏:当 ThreadLocal 实例不再被引用时,它的 key 会变为 null。但问题在于,如果线程本身长时间存在,其维护的 ThreadLocalMap 中的 value 依旧是强引用,即使 key 被回收,value 仍会常驻内存,从而形成"隐形泄漏"。ThreadLocalMap 在执行 set()、remove() 或扩容(rehash)操作时会自动清理 key 为 null 的 entry,但如果线程一直存活且未调用这些方法,value 将永远无法释放。这也是为何在使用 ThreadLocal 存储如连接、缓存等重量级资源后,必须在使用完毕后主动调用 remove() 方法清理,确保不留"垃圾数据"在 Thread 的生命周期中。理解其内存结构与引用机制,是正确使用 ThreadLocal 的关键。
38. 为什么用 ThreadLocal 而不用线程成员变量?
回答:
相比在线程类中定义成员变量,ThreadLocal 能以更低耦合的方式为每个线程维护独立副本,特别适合在线程复用或多线程共享任务实例的场景中使用。
分析:
线程成员变量确实可以实现线程隔离,例如在自定义 Thread 子类中添加变量,确保每个线程持有自己的数据。但这种方式有很强的结构限制,变量必须定义在 Thread 类中,意味着你要扩展线程类来维护这些数据。更麻烦的是,在实际开发中,我们往往通过实现 Runnable 或 Callable 接口来定义任务逻辑,而这两个接口并不绑定线程本身。如果多个线程共享同一个 Runnable 实例,那么其中的成员变量就不具备线程安全性,容易导致并发错误。ThreadLocal 的出现正是为了解决这一问题:它通过每个线程维护一个独立的 Map(ThreadLocalMap)来保存与 ThreadLocal 对象绑定的值,从而无需修改线程结构,也能保证线程隔离。使用 ThreadLocal 可以将变量存储与业务逻辑解耦,在不侵入线程实现的前提下,为每个线程提供独立上下文,是更为优雅和通用的设计。
39 如果你提交任务时,核心线程数已达到配置的数量,这时会发生什么
回答
当核心线程已满时,若使用无界队列(如LinkedBlockingQueue),新任务会无限入队等待,可能引发OOM;若使用有界队列(如ArrayBlockingQueue),任务先入队,队列满后扩容线程至maximumPoolSize,若仍无法处理则触发拒绝策略(默认AbortPolicy抛出异常)。
分析
线程池的任务调度遵循"核心线程→任务队列→非核心线程→拒绝策略"的流程。当核心线程全部忙碌时,新任务的处理方式取决于任务队列的类型。
无界队列(如LinkedBlockingQueue)的特点是队列容量理论无限(默认Integer.MAX_VALUE)。核心线程满后,新任务直接入队等待,不会拒绝任务。但任务积压可能导致内存溢出(OOM),尤其在任务耗时较长或提交速率过高时。这种队列适合任务量可控、需保证所有任务执行的场景,比如后台日志处理。
有界队列(如ArrayBlockingQueue)的队列容量固定,需初始化时指定。核心线程满后,新任务首先尝试入队。若队列未满,任务进入队列等待执行;若队列已满,则根据当前线程数决定是否扩容。若线程数未达maximumPoolSize,线程池会创建非核心线程处理任务;若线程数已达上限且队列仍满,则触发拒绝策略。
默认的拒绝策略是AbortPolicy,直接抛出RejectedExecutionException。其他策略包括CallerRunsPolicy(由提交线程执行任务)、DiscardPolicy(静默丢弃)、DiscardOldestPolicy(丢弃队首任务)。有界队列适合需要限制资源使用的场景,比如高并发请求限流。
关键注意事项包括合理设置线程池参数。无界队列需警惕隐性OOM,建议监控队列积压情况;有界队列需平衡corePoolSize、maximumPoolSize和队列容量,避免频繁触发拒绝策略。非核心线程空闲时会在keepAliveTime后被回收。拒绝策略应根据业务需求选择,例如支付系统可能选用CallerRunsPolicy降级,而日志系统可用DiscardPolicy。