JUC面试题
GQ JUC
JUC
进程、线程、协程的区别
进程负责“资源”,线程负责“执行”,协程负责“更轻量的执行方式”。
进程是什么?(资源隔离+独立内存)
- 进程就是一个程序的运行实例,比如你打开一个微信,就是一个进程。
- 每个进程有自己独立的内存空间、文件句柄等资源,是操作系统中资源分配的最小单位。
- 不同进程之间互不影响,因此稳定性高,但创建和切换成本大。
线程是什么?(执行单元+更轻量)
线程是进程内部的“执行流”,一个进程可以有多个线程。
线程之间共享同一个进程的内存,因此通信非常方便,但安全性要靠加锁保证。
线程是CPU 调度的最小单位,切换比进程轻量,但仍需要 OS 参与调度。
协程是什么?(用户态、极轻、高并发)
协程是比线程更轻量的“用户态调度”的执行方式,不会被 OS 感知。
它把切换时机交给程序自己决定,而不是让 OS 抢占式调度,因此切换开销极低。
常用于大量 I/O 任务,比如 Go 的 goroutine、Java 的虚拟线程。
- 进程:资源独立、安全但重 → “房子”
- 线程:共享资源、切换轻 → “房子里的房间”
- 协程:极轻量、用户态调度 → “房间里的小隔断,不需要 OS 管”
执行成本对比
| 对比项 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 创建成本 | 高 | 中 | 很低 |
| 切换成本 | 高(OS 切换) | 中(OS 切换) | 极低(用户态切换) |
| 内存隔离 | 完全隔离 | 共享 | 共享,逻辑更轻 |
| 调度者 | OS | OS | 程序自身 |
小结
- 进程是操作系统分配资源的单位,每个进程有独立的内存,因此安全但切换成本高。
- 线程是进程里的执行单元,共享进程资源,切换比进程快但需要同步锁来保证安全。
- 协程更轻量,它在用户态调度,不需要 OS 参与,所以切换成本非常低,非常适合 I/O 密集的高并发场景,比如 Go 的 goroutine 或 Java 的虚拟线程。
什么是 Java 中的线程同步?
线程同步其实就是在多线程环境中,保证同一时刻只有一个线程能访问某个共享资源,避免多个线程同时操作同一个资源时出现数据不一致或竞争条件的问题。
线程同步有很多方式,常用的包括 synchronized、ReentrantLock 和原子类,它们的主要目的是保证数据的一致性和线程间的协调。
- synchronized:它可以锁定代码块或者方法,确保同一时刻只有一个线程能执行这个方法或代码块
- ReentrantLock 也是一种很常用的同步工具,它提供了更高的灵活性,比如可以尝试加锁、定时加锁等
- Java 还提供了 Atomic 类系列,比如 AtomicInteger、AtomicLong 等,这些类通过原子操作来保证线程安全,避免了锁的使用
- 原理是这些原子类通过硬件级别的原子操作来实现操作的原子性。
并行与并发,同步与异步
- 同步:需要等待结果返回,才能继续运行
- 异步:不需要等待结果返回,就能继续运行
- 并行:在同一时刻,有多个指令在多个 CPU 上同时执行, 同一时间同时做多件事情的能力。多个人做多件事。
- 并发:在同一时刻,有多个指令在单个 CPU 上交替执行, 同一时间段处理多件事情的能力。一个人做多件事。
Java 中的线程安全是什么意思?
线程安全指的是在多线程环境下,多个线程同时访问某个共享资源时,不会出现数据不一致或修改错误的情况。当一个类或方法是线程安全的时,多个线程可以并发访问它,不需要担心在执行过程中数据会被其他线程改变。它能够确保在执行的过程中,不会出现竞争条件、死锁等问题。
为什么线程安全很重要?
在多线程环境中,多个线程可能会同时对同一数据进行操作。如果没有合适的同步措施,可能会导致数据被意外修改,最终产生不一致的结果。线程安全可以避免这种情况,保证每个线程访问的数据都是正确和一致的。
什么是协程?Java 支持协程吗?
协程其实是比线程更加轻量的执行单元,它允许在执行过程中暂停,然后恢复执行,像是在线程内做任务切换,但比线程效率更高。
jdk19引入了虚拟线程,jdk21确认,可以认为是java对协程的一种实现。
通过虚拟线程,我们可以在同一个线程池里高效地管理成千上万个线程,这样就能大大提高并发性能,特别是在需要大量并发但每个任务执行时间短的场景下。
线程的生命周期在 Java 中是如何定义的?
从 Java API 层面来描述的有六种状态



从 操作系统 层面来描述有五种状态


Java 中线程之间如何进行通信?
在 Java 中,线程之间的通信通常是为了实现多个线程在共享资源上进行协作。主要的方式包括:
- 共享变量
多个线程可以通过访问共享变量来交换信息。为了确保线程安全,必须小心使用共享数据,避免线程间的竞争条件或数据不一致。
- 同步机制
常见的同步机制包括:
- synchronized:用 Java 中的 synchronized 关键字来保证同一时刻只有一个线程能访问共享资源。线程间通过 wait() 和 notify() 等方法进行通信。
- ReentrantLock:提供了类似于 synchronized 的锁机制,允许线程在执行过程中对共享资源进行同步。
- BlockingQueue:提供阻塞队列来控制生产者与消费者的消费模式。
- CountDownLatch:允许一个或多个线程等待,直到其他线程完成某项操作。
- CyclicBarrier:允许多个线程相互等待,直到某个条件满足才继续执行。
- Semaphore:用于控制访问共享资源的线程数量,限制并发线程的数量。
Java 中如何创建多线程?
在Java中创建多线程有几种常见的方式:
- 使用 Thread 类:
通过继承 Thread 类,重写 run() 方法,并调用 start() 来启动线程。这是最基础的方式,但它不够灵活,任务和线程是绑定在一起的,无法重用任务。
1 | public class MyThread extends Thread { |
- 使用 Runnable 配合 Thread:
这种方法通过实现 Runnable 接口来分离任务和线程。创建一个 Runnable 实例,将其传递给 Thread 来启动线程。这种方式更灵活,任务可以被多个线程重用,且更适合与线程池等高级API一起使用。
1 | Runnable r = new Runnable() { |
- 使用 FutureTask 配合 Callable:
当线程需要返回结果时,使用 Callable 接口代替 Runnable,并配合 FutureTask 来执行任务。FutureTask 可以获取任务的返回结果,并且支持异常捕获。
1 | Callable<Integer> task = new Callable<Integer>() { |
- 使用线程池 (ExecutorService):
使用 ExecutorService 管理线程池,可以方便地提交 Runnable 或 Callable 任务,适合处理大量并发任务而不需要手动管理线程。
1 | ExecutorService executor = Executors.newFixedThreadPool(10); |
- 使用 CompletableFuture:
Java 8 引入了 CompletableFuture,提供了更方便的异步任务执行和任务间的依赖关系处理。它是基于线程池(默认使用 ForkJoinPool)实现的,可以链式调用不同的异步操作。
1 | CompletableFuture.runAsync(() -> { |
总结:
- 如果只是简单的创建一个线程,可以直接使用 Thread 类。
- 如果想解耦任务和线程,更灵活且复用性强,推荐使用 Runnable 配合 Thread。
- 如果需要线程执行后有返回值,使用 Callable 和 FutureTask。
- 如果有大量并发任务,使用线程池来管理线程。
- 如果任务之间有依赖关系,使用 CompletableFuture 处理异步任务。
你了解 Java 线程池的原理吗?
线程池:一个容纳多个线程的容器,容器中的线程可以重复使用,省去了频繁创建和销毁线程对象的操作
线程池作用:
- 降低资源消耗,减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
- 提高响应速度,当任务到达时,如果有线程可以直接用,不会出现系统僵死
- 提高线程的可管理性,如果无限制的创建线程,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
线程池的核心思想:线程复用,同一个线程可以被重复使用,来处理多个任务
池化技术 (Pool) :一种编程技巧,核心思想是资源复用,在请求量大时能优化应用性能,降低系统频繁建连的资源开销
线程池的工作原理可以分为几个关键步骤:
- 核心线程数(corePoolSize):这是线程池中最小的线程数量。即使没有任务,线程池也会维持这些线程处于空闲状态。
- 最大线程数(maximumPoolSize):线程池可以创建的最大线程数量。当有大量任务同时到达时,线程池会创建新线程,直到达到最大线程数。
- 空闲时间(keepAliveTime):空闲线程在一定时间内没有任务执行时,线程池会回收它们以节省资源。这个时间是可以调节的。
- 时间单位
- 工作队列(workQueue):这是用于保存提交的任务的队列。如果线程池中所有核心线程都在忙碌,任务就会被放入队列等待。
- 线程工厂(ThreadFactory):用来创建新线程的工厂,可以自定义线程的创建方式。
- 拒绝策略(RejectedExecutionHandler):当线程池中的任务数量达到最大线程数且队列已满时,线程池会采取拒绝策略,通常包括丢弃任务、抛出异常或者让任务在其他地方执行。
常见的线程池类型包括:
- FixedThreadPool:一个固定大小的线程池,适用于负载较为稳定的场景。
- CachedThreadPool:可以根据需要创建新线程,但在空闲时会回收线程,适用于任务数量不确定,且任务执行时间短的情况。
- ScheduledThreadPool:可以定期执行任务或延迟执行任务的线程池。
- SingleThreadExecutor:只有一个线程的线程池,适用于串行执行任务的场景。
总结来说,线程池是为了避免频繁创建和销毁线程的性能问题,同时也提供了灵活的线程管理和任务调度机制,适用于大规模并发处理的场景。
补充:拒绝策略

如何合理地设置 Java 线程池的线程数?
对于如何合理地设置 Java 线程池的线程数,可以通过分析任务的类型来进行优化。通常,任务类型可以分为两类:CPU 密集型任务和I/O 密集型任务。
CPU 密集型任务:这些任务主要依赖 CPU 进行计算,如数学运算等。在这种情况下,线程数的设置应根据 CPU 核心数来决定。为了充分利用 CPU 的计算资源,线程池的核心线程数可以设置为 CPU 核心数 + 1,这样可以充分利用 CPU 的空闲时间,避免过多线程的竞争。
I/O 密集型任务:这类任务大多依赖 I/O 操作,如文件读写、网络请求等。在进行 I/O 操作时,线程并不占用 CPU 资源,因此可以增加线程池的线程数。通常可以将线程数设置为 2 倍 CPU 核心数,因为大多数 I/O 操作会阻塞线程,这时可以充分利用 CPU 执行其他任务。
总结:
- 对于 CPU 密集型 任务,线程数应该设置为 CPU 核心数 + 1,这样可以避免线程过多导致的上下文切换和 CPU 资源浪费。
- 对于 I/O 密集型 任务,线程数可以设置为 2 倍 CPU 核心数,这样可以充分利用线程池的线程而不造成过多阻塞。
如果业务中同时有这两种类型的任务,推荐根据任务的比例来动态调整线程池的大小。
Java 线程池有哪些拒绝策略?
在 Java 中,线程池有四种拒绝策略:
- 默认是 AbortPolicy,也就是当线程池满了,系统会抛出异常,提醒你有任务无法被处理。
- CallerRunsPolicy 是让提交任务的线程(调用者线程)自己执行任务。适合你希望通过减缓任务提交的速度来避免系统过载的场景。
- DiscardOldestPolicy 则会丢弃队列中最旧的任务,保留新的任务继续执行。如果你希望保留当前任务并丢弃掉早些提交的任务,这个策略就很合适。
- DiscardPolicy 是最简单粗暴的,它会直接丢弃任务,不做任何处理,适合在高负载时只保留最重要的任务。
Java 并发库中提供了哪些线程池实现?它们有什么区别?
在 Java 的并发库中,主要有五种常见的线程池实现,它们分别是:
- FixedThreadPool:这是一个固定大小的线程池。线程数是固定的,如果有任务来时,线程池会先检查当前是否有空闲线程,如果有就执行任务,如果没有,它会将任务排队等待。适合任务量稳定的场景。
- CachedThreadPool:这个线程池可以动态调整线程数。线程池会根据任务需求创建新线程,如果现有的线程没有被使用,它会在60秒后自动回收。这种线程池适合处理大量短时间任务的场景,但如果任务量过多,可能会导致内存溢出(OOM)。
- SingleThreadExecutor:这就是一个单线程池,它只会有一个线程来执行所有任务。如果一个任务执行失败,后续的任务也会被阻塞,直到当前任务完成。适用于需要确保任务按顺序执行的场景。
- ScheduledThreadPool:这是一个支持定时任务的线程池。它可以定期或延迟执行任务,适用于定时执行任务或延时执行的场景。
- WorkStealingPool:这个线程池是基于工作窃取算法设计的。它是从多个线程中窃取工作来优化任务执行,适用于任务之间相对独立,且负载不均匀的情况。
总结:如果你有一堆任务并且任务量比较大,固定线程池和缓存线程池更适合;如果任务量不大,但需要定时执行,使用 ScheduledThreadPool。如果任务之间有很强的顺序性,可以使用 SingleThreadExecutor。如果任务负载不均匀,WorkStealingPool 是一个不错的选择。
Java 线程池核心线程数在运行过程中能修改吗?如何修改?
在 Java 中,我们是可以动态地调整线程池的核心线程数的。可以通过调用 ThreadPoolExecutor 的 setCorePoolSize() 方法来修改核心线程数。需要注意的是,减少核心线程数时,空闲的多余线程不会立刻回收,只有等到它们一段时间没有任务执行后才会被回收。而且,这个修改不会影响正在执行的任务,所以它是即时生效的。
Java 线程池中 shutdown 与 shutdownNow 的区别是什么?
- shutdown() 用于优雅地关闭线程池,它会停止接受新任务,但会继续执行队列中已提交的任务,直到全部完成为止。
- shutdownNow() 会立即尝试停止所有任务,返回当前尚未执行的任务列表,并尽可能通过中断线程来停止正在执行的任务。如果任务无法处理中断,它们仍然可能继续执行,直到任务完成。
这两种方法主要的区别在于是否会强制中断正在执行的任务,shutdown() 是平稳退出,而 shutdownNow() 是立即中止。
Java 线程池内部任务出异常后,如何知道是哪个线程出了异常?
在Java的线程池中,如果线程执行的任务抛出异常,默认情况下线程池并不会主动告诉你是哪个线程出了问题,但我们有几种方式可以捕获这个异常信息。
首先,如果你使用 ThreadFactory 来创建线程池,可以自定义 UncaughtExceptionHandler 来处理每个线程中的异常。这样,线程在遇到未处理的异常时,就会调用 UncaughtExceptionHandler,你可以在其中记录线程的异常信息。
其次,在线程任务执行时,我们可以使用 Future。当使用 submit() 提交任务时,我们可以通过返回的 Future 对象来检查任务是否执行成功,如果任务有异常,可以通过 get() 方法时候try-catch捕获异常。
最后,在run方法内部,我们还可以使用 try-catch 来捕获异常,避免任务因为异常直接失败。在捕获异常后,我们可以记录或者处理异常,也能确保任务不会导致线程池停止。
总的来说,我们可以通过 ThreadFactory 来为每个线程设置异常处理器,使用 Future 对象来捕获任务异常,并在任务内部加入 try-catch 来处理异常,确保线程池中的任务都能被监控和管理。
Java 中的 DelayQueue 和 ScheduledThreadPool 有什么区别?
在 Java 中,DelayQueue 和 ScheduledThreadPoolExecutor 都是用于处理延迟任务的工具,但它们的实现原理和使用场景有所不同。
- DelayQueue:
DelayQueue 是一个阻塞队列,它基于元素的延迟时间来控制任务的执行顺序。每个元素都有一个指定的延迟时间,元素不会立刻被处理,只有在延迟时间到了之后,它才会从队列中被取出执行。
它通常与 ReentrantLock 和 Condition 配合使用,确保任务在合适的时间被执行。
- ScheduledThreadPoolExecutor:
这是一个专门用于执行定时任务和周期任务的线程池。它允许你定期执行任务或延迟执行任务,常用于周期性任务的调度。
ScheduledThreadPoolExecutor 提供了比 DelayQueue 更灵活的任务调度功能,比如支持固定延迟任务、定时执行等。
什么是 Java 的 Timer?
Java 中的 Timer 是用来定时执行任务的工具,通常和TimerTask搭配使用,TimerTask是需要执行的任务。你可以用 schedule() 来延时执行任务,或者用 scheduleAtFixedRate() 来做周期性任务。它内部有一个专门的线程去管理这些任务。
不过,Timer 是单线程的,如果任务的执行时间比预定的时间长,可能会影响后续任务的执行,造成任务延迟。因此在高并发场景下,Timer 可能不是最优选择,通常我们会推荐使用 ScheduledExecutorService 来替代 Timer,因为它提供了更高效的多线程任务调度。
你了解时间轮(Time Wheel)吗?有哪些应用场景?
时间轮是一种高效的定时任务调度算法,它通过将时间切分为多个固定的时间槽,并将任务分配到这些时间槽中,来实现任务的定时执行。当时间轮转到某个槽时,槽内的任务就会执行。这个算法特别适合于需要处理大量定时任务的场景,因为它避免了频繁的时间比较,提升了效率。比如在高并发的网络服务中,时间轮可以用来优化定时任务的调度,减少系统的负担。
你使用过哪些 Java 并发工具类?

什么是 Java 的 Semaphore?
在 Java 中,Semaphore(信号量)是一个用于控制访问共享资源的工具类。它通过维护一定数量的许可证来管理线程访问资源的权限,确保在任何时刻,只有限定数量的线程能够访问资源。线程需要先通过 acquire() 获取许可证才能访问资源,使用完毕后必须通过 release() 来释放许可证。它支持两种模式:公平模式,线程按照顺序获取许可,避免饥饿;非公平模式,则是线程可以抢先获取许可证,可能导致不公平的资源分配。
什么是 Java 的 CyclicBarrier?
CyclicBarrier 是一种同步工具类,允许一组线程在执行某些任务时相互等待,直到所有线程都到达某个指定的“屏障点”时,才会继续执行。它通常用于需要协调多个线程按一定顺序执行的场景。
举个通俗的例子就是:比如说打篮球约了6个人,如果6个人都没有到齐之前,先到的人需要继续等待,直到最后一个人到才能开始比赛
工作原理:
CyclicBarrier 内部维护一个计数器,表示需要等待的线程数。每当一个线程执行 await() 方法时,计数器就会减1,直到计数器的值为0,所有等待的线程才会被唤醒,继续执行。
可以通过传入一个 Runnable 的 barrierAction 来定义屏障到达时要执行的操作,这个操作会在所有线程到达屏障后执行。
应用场景:
- 并行任务协调:比如,多个线程执行某些计算任务,并且在每个阶段后需要等待其他线程,保证每个阶段都同步进行。
- 批量任务处理:在分布式计算或者并行任务处理时,可以用来协调多个线程的工作,确保所有线程都完成了某个阶段的工作后才进入下一个阶段。
什么是 Java 的 CountDownLatch?
CountDownLatch 是一个同步辅助工具,属于 JUC(Java 并发工具包)的一部分,它允许一个或多个线程阻塞等待其他线程完成某项任务后再执行。比如我们在获取某个连接之前,需要等待这个连接初始化完毕 这个场景,就可以用CountDownLatch完成、
底层也是通过一个计数器来实现,每当一个线程完成工作时,就会减少一个数字,当计数器的值减到零时,所有等待的线程将被唤醒并继续执行。
主要功能:
- 等待其他线程完成任务:通过 await() 方法,线程会等待其他线程完成其任务。
- 减少计数器:当某个线程完成工作时,调用 countDown() 方法将计数器的值减一。
- 所有线程完成时再执行:当计数器的值减到零时,所有等待的线程会被唤醒,继续执行。
应用场景:
- 并行任务的协同工作:比如有多个线程并行执行任务,主线程需要等所有线程都完成后再继续执行。CountDownLatch 很适合这种场景,比如启动多个线程进行并行处理,主线程在所有线程完成任务后再进行合并处理。
比较CountDownLatch 和 CyclicBarrier 的区别
适用场景不同:
- CyclicBarrier适合在一组线程互相等待达到共同的状态然后同时开始或继续执行后续操作
- CountDownLatch适合于一个或多个线程等待其他线程执行完成某个操作后再继续执行
重用性不同:
- CyclicBarrier 是可以重用的,所有线程到达屏障后自动重置。
- CountDownLatch 一次性使用,一旦计数器归零就不能再使用。
简单记忆:CountDownLatch是”等你们完成”,CyclicBarrier是”一起行动”!
什么是 Java 的 StampedLock?
StampedLock 是 Java 8 新增的一种锁机制,它通过引入乐观读锁提高了性能,特别适用于读多写少的场景。它有三种锁模式:写锁、悲观读锁和乐观读锁。
写锁
- 独占锁,类似于 ReentrantLock 的写锁。它确保其他线程不能获取到写锁或读锁。
悲观读锁
- 共享锁,允许多个线程同时获取读锁,但不允许有线程获取写锁。
乐观读锁
- 不加锁,允许线程在没有竞争的情况下进行快速读。只有在检测到写操作发生时,它才会回退到悲观读锁。
最特别的是,StampedLock 还会返回一个时间戳,代表当前锁的状态,线程可以利用这个时间戳来判断锁是否还有效,从而决定是否继续执行操作。
什么是 Java 的 CompletableFuture?
在 Java 8 中,CompletableFuture 是为了简化异步编程而引入的工具。你可以把它想象成一个可以在后台执行任务的对象,它支持异步执行,也就是你可以启动一个任务,它会在后台进行计算,计算结果一旦出来,你可以拿到它做后续处理。
最核心的几个特性是:
- 异步执行:使用 runAsync() 或 supplyAsync() 方法,你可以让任务在后台异步执行,这样不会阻塞主线程。
- 任务组合:你可以使用 thenApply() 或 thenAccept() 等方法在任务完成后进行处理,支持链式调用。
- 并行任务:CompletableFuture 允许你同时执行多个任务,并且可以通过 .allOf() 来合并这些任务的结果。
- 异常处理:如果异步任务执行过程中出错,你可以使用 exceptionally() 来处理异常,避免程序崩溃。
CompletableFuture 的优势在于它让你可以轻松地处理异步操作,特别是当你需要执行多个任务并且希望它们能并行执行时,非常适合用它。而且,通过链式调用,你可以简化代码逻辑,不需要复杂的回调函数。”
总结
CompletableFuture 是处理异步编程非常强大的工具,它提供了各种方法来执行异步任务,管理结果,处理异常,并支持多个任务的并行执行,特别适合在需要高效处理并发任务的场景中使用。
什么是 Java 的 ForkJoinPool?
ForkJoinPool 是 Java 7 引入的一个线程池,主要用来处理大规模的并行任务。它采用了“分而治之”的方式,把一个大任务分解成多个小任务进行并行处理,所有任务完成后再合并结果。它的工作方式是通过两个操作:fork 来分解任务,join 来合并结果。
ForkJoinPool 使用了工作窃取算法,ForkJoinPool是WorkStealingPool的底层,这意味着如果有线程空闲,它会去偷取其他线程没有完成的任务,从而提高并行度和资源利用率。常见的相关任务类是 RecursiveTask(有返回值的任务)和 RecursiveAction(无返回值的任务)。
这个池子特别适合用于需要大量并行处理的计算任务,比如大数据处理或者需要执行递归算法的场景。
如何在 Java 中控制多个线程的执行顺序?
在 Java 中,控制多个线程的执行顺序有多种方式,常用的方法包括:
CompletableFuture 的
thenRun方法:- 如果你有多个任务(例如 T1、T2、T3),你可以使用
thenRun方法来保证这些任务按顺序执行。每个任务会在上一个任务完成后才开始执行。这种方法适合于处理并发任务,并且保证顺序。
示例:
1
2
3CompletableFuture.runAsync(() -> { doTask1(); })
.thenRun(() -> { doTask2(); })
.thenRun(() -> { doTask3(); });- 如果你有多个任务(例如 T1、T2、T3),你可以使用
synchronized + wait/notify:
- 使用
synchronized来加锁,结合wait和notify来控制线程间的执行顺序。wait用于让一个线程暂停,直到收到其他线程的通知(notify)才继续执行。
- 使用
ReentrantLock 配合 Condition 的
await、signal、signalAll:- 通过
ReentrantLock和Condition,你可以更加细粒度地控制线程的执行顺序。例如,使用await使线程等待,直到其他线程通过signal或signalAll通知它们继续执行。
- 通过
CountDownLatch:
CountDownLatch可以用来让多个线程等待直到其他线程完成指定的工作。通过调用countDown()来减少计数,直到计数为零,所有线程才会继续执行。
CyclicBarrier:
CyclicBarrier允许一组线程在一个公共点等待,直到所有线程都到达这个点后再继续执行。适用于并行处理多任务,确保每个线程都完成到达某个阶段后再继续。
Thread 类的
join()方法:join()方法可以让一个线程等待另一个线程完成后再执行。通过调用join(),主线程可以等待其他线程执行完后再继续。
LockSupport 的
park和unpark方法:LockSupport提供了更底层的控制方法,park()用来挂起线程,unpark()用来恢复线程的执行。
Semaphore:
Semaphore控制并发的线程数量,适用于控制访问某些资源的线程数量。通过设置许可数来限制并发线程的数量。
ExecutorService 的单线程执行:
- 如果不关心线程池中的线程,
ExecutorService提供了顺序执行任务的功能,可以将任务提交给一个单线程执行。
- 如果不关心线程池中的线程,
小结:
- 最简洁的方式是
Thread.join(),非常直接,适合顺序依赖的场景。 - 如果你需要更复杂的控制,比如多个线程并发执行后再同步,使用
CountDownLatch或CyclicBarrier更合适。 - 你可以根据任务的复杂度选择适合的工具,比如
ReentrantLock和Condition提供更细粒度的控制。
你使用过 Java 中的哪些阻塞队列?
Java 提供了几种常见的阻塞队列,主要用于在多线程环境下处理线程间的任务传递。常见的有:
- ArrayBlockingQueue:这是一个基于数组实现的队列,容量固定。生产者往队列里放数据时,如果队列满了,就会被阻塞;消费者取数据时,如果队列空了,也会被阻塞。适用于生产者-消费者模型。
- LinkedBlockingQueue:这是基于链表实现的队列,可以是有界的也可以是无界的。它在队列满时阻塞生产者,在队列空时阻塞消费者。
- PriorityBlockingQueue:这个队列没有容量限制,且任务按优先级排队。优先级高的任务会先被处理,适合需要优先级调度的场景。
- DelayQueue:这是一个专门处理延迟任务的队列,只有当任务的延迟时间到达时,才能取出。这适合用在需要定时执行的任务中。
- SynchronousQueue:这个队列没有任何容量,每一个put操作必须等待一个take操作。因此,它常用于线程之间直接的任务传递。
- TransferQueue:它是在LinkedBlockingQueue的基础上新增了transfer方法的队列。put操作不会立即阻塞,直到有一个take操作时,队列才会执行传递任务。
你使用过 Java 中的哪些原子类?
在多线程编程中,有时候我们需要对共享数据进行操作,这时为了避免线程冲突和数据不一致的问题,我们通常会使用 Java 中的原子类。它们通过内置的原子性操作来确保线程安全,避免了传统的加锁操作。
最常用的原子类包括:
- AtomicInteger 用来原子地增加或减少整数,比如在计数器等场景下;
- AtomicLong 是 AtomicInteger 的长整型版本;
- AtomicBoolean 用于处理布尔值,适用于类似于标志位的场景;
- AtomicReference 适用于引用类型的数据,能保证在多线程环境中更新对象的引用;
- AtomicStampedReference 则解决了 ABA 问题,它通过添加一个版本号来避免由于值相同导致的误判断。
- 如果需要对数组中的每个元素进行类似的原子操作,可以使用 AtomicIntegerArray 和 AtomicLongArray。
你使用过 Java 的累加器吗?
前提:为什么需要累加器?
AtomicLong 在并发很高时,所有线程都会去竞争同一个变量的 CAS 操作,导致严重的总线争用(CPU cache contention),性能急剧下降。
什么是java中的累加器?
在 Java 中,“累加器”通常指的是 LongAdder 和 DoubleAdder 这两个类。
它们是 JDK 1.8 引入的,目的是在高并发场景下提升计数性能,是对传统 AtomicLong、AtomicDouble 的优化。
LongAdder和DoubleAdder它们内部通过维护多个分段计数单元(Cell),让不同线程通过CAS更新不同的单元,最后再汇总求和,从而降低 CAS 冲突,提升性能。
什么是 Java 的 CAS(Compare-And-Swap)操作?
CAS,全称是 Compare And Swap(比较并交换),是一种实现并发中原子操作的机制。
在修改一个变量时,CAS 会:
1.先读取变量当前的值(内存值);
2.与期望值比较;
- 2.1如果相等,则将变量更新为新值;
- 2.2如果不相等,说明其他线程修改过该值,则更新失败,通常会选择自旋重试。
CAS 的底层是由 CPU 的原子指令(如 x86 的 CMPXCHG) 实现的,整个过程是不可中断的,因此能保证操作的原子性。
CAS 的优点
- 无锁并发:在不使用传统锁的情况下,也能保证线程安全,提高并发性能。
- 原子性保障:借助硬件级别的指令支持,确保修改操作不会被打断。
CAS 的缺点与解决方案
1.ABA 问题
- 问题:一个变量从 A → B → A,CAS 检查时发现值没变,但实际上经历了变化。
- 解决:使用版本号机制,如AtomicStampedReference,在 CAS 时同时比较值和版本号。
2.自旋开销大
- 问题:如果多个线程频繁失败,会导致长时间自旋,浪费 CPU 资源。
- 解决:在一些高并发组件(如 SynchronousQueue)中限制自旋次数或结合锁机制使用。
CAS+锁这一套组合挺常见比如CourrentHashMap就是这样实现的,CAS 用于无锁写入,如果冲突严重再退化为锁定特定桶的头结点。
3.只能操作一个共享变量
问题:CAS 只能针对单一变量进行原子操作。
解决:使用 AtomicReference 将多个变量封装为一个对象,实现复合 CAS。
总结一句话
CAS 是一种基于 CPU 原子指令的无锁机制,通过比较内存值和预期值,一致则更新,否则重试。
它的优点是高并发、无锁和线程安全,但缺点是可能出现 ABA问题、自旋开销大、无法操作多个变量,可以通过版本号或 AtomicReference 来优化
说说 AQS 吧?
一.AQS 是什么
AQS,全称 AbstractQueuedSynchronizer(抽象队列同步器),是 JUC(java.util.concurrent)包中构建锁和同步器的基础框架。
AQS起到了一个抽象、封装的作用,将一些排队、入队、加锁、中断等方法提取出来,便于其他相关JUC锁的使用,具体加锁时机、入队时机等都需要实现类自己控制(它通过统一的同步状态管理和队列机制,简化了各种同步器(如锁、信号量、栅栏)的实现)。
二.AQS 的核心思想
AQS 通过维护一个同步状态变量(state)和一个FIFO 等待队列,来管理多个线程对共享资源的竞争。
- 同步状态(state)
是一个 volatile int 变量,表示资源的占用情况。
对于独占锁:state = 0 表示未加锁,state = 1 表示已加锁。
对于共享锁:state 表示可同时获取锁的线程数(例如 Semaphore 的剩余许可数)。
等待队列(FIFO)
当线程获取锁失败时,会被封装为一个 Node 节点加入队列尾部。
当锁被释放时,AQS 会唤醒队列中第一个等待线程尝试获取锁。
队列是一个基于 CLH(Craig, Landin, and Hagersten) 算法实现的双向链表结构,保证线程排队公平。
三、AQS 的工作流程
线程尝试获取锁 → 修改 state(CAS 操作)。
如果失败 → 封装为 Node 节点加入队列,进入等待状态。
当前持锁线程释放锁后 → AQS 唤醒队列中的下一个节点线程。
这样,AQS 就实现了一个通用的“排队获取锁”机制。
四、AQS 的典型实现类
基于 AQS 的同步组件包括:
独占模式:ReentrantLock
共享模式:Semaphore、CountDownLatch、ReentrantReadWriteLock
条件队列:Condition(基于 AQS 的条件队列实现)
五.总结一句话(精简口述版)
AQS 是 JUC 中实现各种锁和同步工具的核心框架。
它通过一个 state 状态变量和一个 FIFO 队列来管理线程的竞争。
加锁失败的线程会进入队列排队,释放锁后再唤醒队首线程。
常见的实现类包括 ReentrantLock、Semaphore、CountDownLatch 等。
一句话记忆口诀:
AQS:一个 state + 一个队列,构建所有并发锁。
Java 中 ReentrantLock 的实现原理是什么?
ReentrantLock 实现原理
一、ReentrantLock 是基于 AQS 的实现
ReentrantLock 是 Java 中基于 AQS(AbstractQueuedSynchronizer) 的锁实现。
它支持以下特性:
可重入性:同一线程可以多次获取锁。
公平与非公平:支持公平锁和非公平锁的选择。
可中断:可以响应中断,避免死锁。
二、ReentrantLock 的内部结构
内部通过一个state变量和两个队列(同步队列和等待队列)来实现
1.state 变量
ReentrantLock 使用一个 state(通常是一个整数)来记录锁的状态。
例如,state = 0 表示锁没有被占用,state > 0 表示锁已被线程占用,state 值还表示该线程持有锁的次数。
- 同步队列(Sync Queue)
用于存放所有等待获取锁的线程,是一个双向链表。线程在获取锁失败时会被加入同步队列,按照 FIFO 排队,直到锁可用。
3.等待队列(Condition Queue)
用于存放等待特定条件的线程,即使用 Condition 时会涉及到的队列,存放需要等待某些条件才能继续执行的线程。它是一个单向链表。
三、锁的公平性机制
- 公平锁(Fair Lock):
当请求锁时,公平锁会判断当前线程是否是同步队列的第一个线程。如果是,它会尝试获取锁。如果不是,它会等到前面的线程释放锁后再尝试获取。公平锁避免了“饥饿”问题,保证线程按照请求的顺序获取锁。
- 非公平锁(Non-fair Lock):
非公平锁则不会强制按照队列顺序获取锁,它会直接尝试获取锁,如果失败才会加入同步队列。这使得非公平锁获取锁的速度更快,但可能会导致一些线程长时间得不到锁(即“线程饥饿”问题)。
四、获取锁的过程
在 公平锁 中,获取锁时:
首先判断当前线程是否为同步队列的第一个线程,或者同步队列是否为空。
如果是第一个线程,则尝试获取锁。
否则,当前线程会加入队列,等待前一个线程释放锁后再尝试。
在 非公平锁 中,获取锁时:
- 直接尝试获取锁,不做任何排队判断,如果获取失败,才加入同步队列进行排队。
五、总结
ReentrantLock 是一个非常强大的锁,它不仅提供了可重入性,还通过 AQS 实现了灵活的线程排队机制。
通过选择 公平锁 或 非公平锁,可以根据具体场景优化性能。
精简口述版
ReentrantLock 是基于 AQS 实现的,支持可重入、可中断以及公平/非公平模式。
它通过一个 state 变量和两个队列(同步队列、等待队列)管理线程。同步队列用于排队获取锁的线程,等待队列则用于存放等待特定条件的线程。
公平锁会优先考虑队列中第一个线程,而非公平锁则直接尝试获取锁,效率更高,但可能导致线程饥饿。
Java 的 synchronized 是怎么实现的?
一、synchronized 的实现原理
synchronized 是 Java 中最基础的同步手段,它依赖 JVM 内部的 Monitor(监视器锁) 来实现线程同步。
每个对象在 JVM 层面都与一个 Monitor 相关联,当线程获取对象锁时,实际上就是获取了这个对象的 Monitor。
同时,synchronized 的加锁信息存储在对象的 对象头(Object Header) 中。
对象头包含 Mark Word 字段,其中记录了锁的状态(无锁、偏向锁、轻量级锁、重量级锁等)以及线程 ID 等信息。
二、synchronized 在字节码层面的实现
1.修饰方法
当 synchronized 修饰方法时,编译器会在该方法的字节码中添加一个 ACC_SYNCHRONIZED 标志位。
当线程调用该方法时,JVM 会自动尝试获取该方法所属对象的 Monitor。
如果获取成功,执行方法体;否则线程会阻塞等待。
方法执行完毕后,JVM 会自动释放锁。
- 修饰代码块
当 synchronized 修饰代码块时,编译后的字节码会在同步代码块前后生成:
monitorenter(进入同步块,加锁)
monitorexit(退出同步块,释放锁)
这两个字节码指令配合使用,确保线程执行完同步代码后锁能被正确释放。
三、synchronized 的锁优化机制
从 JDK 1.6 开始,JVM 对 synchronized 进行了多次性能优化,引入了锁的四种状态:
- 无锁(Unlocked)
- 偏向锁(Biased Lock)
- 轻量级锁(Lightweight Lock)
- 重量级锁(Heavyweight Lock)
锁会根据竞争情况在这几种状态之间自动升级,以提升并发性能。
四、总结一句话(精简口述版)
synchronized 是基于 JVM 实现的同步机制,通过对象的 Monitor(监视器锁) 来保证线程安全。
修饰方法时,会在方法标志中添加 ACC_SYNCHRONIZED 标志;
修饰代码块时,通过 monitorenter / monitorexit 字节码实现加锁和解锁。
另外,从 JDK1.6 起,JVM 还通过偏向锁、轻量级锁等机制对它进行了性能优化。
一句话口诀:
Monitor + Object Header + 字节码指令 = synchronized 的底层实现
synchronized 优化原理(小故事)
打印机共享队列模型
背景:
你们公司有一台共享打印机(表示临界资源),程序员小王和其他同事每天都要用它打印资料。为了不冲突,公司安排了三种打印控制机制来「优化效率」,正好对应 JVM 中的三种锁状态。
锁优化三种形态对比类比
| JVM 锁类型 | 类比打印控制机制 | 特点 |
|---|---|---|
| 偏向锁(Biased) | 打印机记住上一个用户是谁 如果是同一个人再次使用,不做任何检查,直接打印 | 无竞争时性能最好(零开销) |
| 轻量级锁(Lightweight) | 多人要打印,先排队轮询查看是否空闲,只要打印机还没结束上一个人的工作,就自旋等待几次 | 低冲突下仍能保持高性能(无阻塞) |
| 重量级锁(Heavyweight) | 冲突太多了,打印任务就被挂起/唤醒,由操作系统调度谁先打印 | 冲突激烈时保障正确性,但性能最差 |
场景详解
偏向锁(Biased Lock)
- 小王是第一个用打印机的人
- 打印机会记住小王的身份
- 后面只要小王再次来打印,打印机就直接开工,不检查排队系统,因为它“偏向”小王
就像对象 MarkWord 中记录了某个线程的 ID,只有当其他线程来竞争才撤销偏向锁
轻量级锁(Lightweight Lock)
- 小王打印时,小李也来了
- 打印机会说:“先别挂起,等等看小王是不是很快结束”
- 小李就在打印机旁边“自旋等待”
- 如果小王很快结束了,小李就立即上,没有阻塞、也没有上下文切换
自旋锁本质:用 CPU 忙等来换取线程不挂起
重量级锁(Heavyweight Lock)
- 现在来了十几个人都要打印,等太久了
- 打印机会说:“别等了,我排个队号,通知你们一个个来”
- 系统就开始用 阻塞 → 唤醒 → 再阻塞的方式调度线程
操作系统介入调度,线程切换代价高,效率最低
最后总结:
| 情况 | JVM锁 | 打印类比 | 特点 |
|---|---|---|---|
| 单线程使用资源 | 偏向锁 | 打印机只认上一次的使用者 | 零开销,最快 |
| 少量线程争用 | 轻量级锁 | 排队观察是否释放 | 快速尝试获取锁 |
| 多线程激烈争用 | 重量级锁 | 线程挂起等待系统调度 | 安全但慢 |
Synchronized 修饰静态方法和修饰普通方法有什么区别?
synchronized 修饰静态方法:锁住的是类的 Class 对象,因此所有该类的实例共享同一把锁,多个线程调用同一个类的静态同步方法时会互斥执行。
synchronized 修饰实例方法:锁住的是当前对象实例,每个实例有自己独立的锁,不同实例之间可以并发执行同步方法;但同一个实例的多个线程调用实例方法时会互斥执行。
总结:
synchronized 修饰 静态方法 时锁住的是 类级别的锁。
synchronized 修饰 实例方法 时锁住的是 实例级别的锁。
Java 中的 synchronized 轻量级锁是否会进行自旋?
jdk8中轻量级锁 CAS失败了之后,会直接进入重量级锁膨胀过程。
重量级锁竞争失败会有自旋操作,轻量级锁没有这个动作。
Synchronized 能不能禁止指令重排序?
synchronized 本身并不能完全禁止指令重排序。但是它能够通过 内存屏障 保证 线程的可见性和有序性,在加锁和解锁操作时,JVM 会插入合适的内存屏障来保证语义上的顺序性。
volatile 更能保证禁止指令重排序,并且它保证变量的 可见性,但是 不能保证原子性,也无法像 synchronized 一样提供 互斥锁。
总结:是否可以禁止指令重排序?
synchronized 能在一定程度上防止指令重排序,但不如 volatile 那么直接。
如果你需要更严格地禁止指令重排序,使用 volatile 更加合适。
当 Java 的 synchronized 升级到重量级锁后,所有线程都释放锁了,此时它还是重量级锁吗?
如果在重量级锁状态下,所有线程都释放了锁,那么该锁就会恢复为可用状态,也就是没有任何线程持有该锁(无锁状态)。
当下次有线程尝试获取锁时,JVM 会根据当前的线程竞争情况决定是否继续使用重量级锁,或者降级为轻量级锁甚至偏向锁。
什么是 Java 中的锁自适应自旋?
自适应锁是一种用来优化并发性能的锁机制,特别是在低竞争环境下。当多个线程争用同一个锁时,JVM 会首先尝试自旋,让线程在短时间内不断尝试获取锁,避免频繁的上下文切换。
在锁竞争较轻的情况下,自旋会节省CPU资源,并且可以提高并发性能。
如果自旋失败,JVM 会根据当前的竞争情况动态调整自旋次数,避免线程一直在自旋而浪费过多的CPU时间。
自适应锁通过这种方式,减少了阻塞锁的开销,但如果线程竞争激烈,最终会转为重量级锁,并导致线程的阻塞和上下文切换。
优点:
在低竞争的环境中,避免了线程频繁的挂起和唤醒,提高了效率。
相比于传统的锁,能更有效地减少锁的竞争。
缺点:
- 自旋会浪费CPU资源,尤其在锁竞争非常激烈的情况下,线程会一直在自旋等待,反而会增加CPU的负担。
Synchronized 和 ReentrantLock 有什么区别?
1.实现机制:
Synchronized 是 Java 中的关键字,基于 JVM 层面通过 Monitor(监视器锁) 实现同步。
ReentrantLock 是 java.util.concurrent 包下的类,基于 AQS(抽象队列同步器) 实现,通过自定义同步器来实现锁的获取和释放。
2.锁的获取和释放:
Synchronized 是隐式获取锁,线程进入同步代码块或方法时自动获取锁,执行完毕自动释放锁。
ReentrantLock 需要显式调用 lock() 获取锁,调用 unlock() 释放锁,灵活性更强。
3.锁的公平性:
Synchronized 默认是非公平锁,线程的获取顺序不一定按照请求顺序。
ReentrantLock 默认是非公平锁,也可以通过构造函数设置为公平锁,保证按照请求顺序获取锁,但可能会降低性能。
4.锁的重入性:
Synchronized 是可重入锁,同一个线程可以多次获取同一个锁。
ReentrantLock 也是可重入锁,通过内部计数器管理重入次数,直到重入次数为0时才真正释放锁。
5.性能:
Synchronized 在 JDK1.6 之前性能较差,但 JDK 1.6 之后进行了优化,支持偏向锁、轻量级锁等。
ReentrantLock 功能更强大,支持条件变量、读写锁等,适用于复杂的并发场景。
Volatile 与 Synchronized 的区别是什么?
volatile 和 synchronized 都是 Java 中用于实现线程安全的机制,但是它们的工作原理和使用场景有很大的不同。
volatile变量:volatile用于保证某个变量的可见性。它能确保当一个线程修改了volatile变量的值后,其他线程能够立即看到这个更新。也就是说,它确保所有线程读取该变量时,会直接从主内存中读取,而不是从线程自己的缓存中读取。- 但是,
volatile只保证了“可见性”,并不能保证“原子性”。比如你想执行i++这样的操作,虽然volatile会保证变量值的最新性,但由于这不是一个原子操作,多个线程同时修改同一个volatile变量时,仍然会出现竞争条件。
synchronized关键字:synchronized是用来修饰方法或代码块的,它通过加锁的方式确保同一时刻只有一个线程可以执行该方法或代码块。这不仅保证了变量的可见性,还保证了操作的“原子性”。- 使用
synchronized会引起性能开销,因为它需要获取和释放锁。锁的获取和释放会导致线程阻塞,尤其是多个线程争用锁时,可能会发生上下文切换,影响性能。
关键区别总结:
volatile主要保证变量的可见性,但不能保证原子性。synchronized不仅保证变量的可见性,还能保证操作的原子性,但会导致线程的性能开销,因为它涉及到加锁和释放锁。
如何优化 Java 中的锁的使用?
优化 Java 中锁的使用
- 首先要减少锁的持有时间。尽量缩小锁定范围,避免长时间占用锁资源。
- 其次,减少锁的粒度,可以通过加细粒度的锁来提高并发性,例如使用读写锁(ReadWriteLock) 来处理读多写少的场景。
- 最后我们要减少锁的使用:而在一些简单的并发场景中,我们可以用 CAS 操作或者原子类(比如 AtomicInteger)来代替传统的锁,避免不必要的性能损失。
你了解 Java 中的读写锁吗?
读写锁(ReadWriteLock)是为了优化多线程环境下的读操作而设计的。Java 中的 ReentrantReadWriteLock 是实现该锁的一个主要工具。
- 读锁(Read Lock):允许多个线程同时读取共享资源,只要没有线程在写入。这就避免了读线程之间的互斥,提升了系统的并发性。
- 写锁(Write Lock):是独占锁,在一个线程获取写锁后,其他所有读写操作都无法进行,直到该线程释放写锁。这保证了数据的一致性。
ReentrantReadWriteLock 允许一个线程在获取写锁后,也可以重新获取它,这使得它支持递归锁(Reentrant)。而且,它还支持将读锁和写锁分开,极大提升了多线程读取时的效率。
总结:
- 读锁和写锁分开,多个读线程可以并发访问,写线程必须等所有读线程完成后才能访问。
- 适用场景:特别适合读多写少的场景,比如缓存、日志等高并发读取的系统。
什么是 Java 内存模型(JMM)?
Java 内存模型是 Java Memory Model(JMM),本身是一种抽象的概念,实际上并不存在,描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式.
为什么需要JMM?
- 屏蔽各种硬件和操作系统的内存访问差异,实现让 Java 程序在各种平台下都能达到一致的内存访问效果
- 规定了线程和内存之间的一些关系
JMM 主要关注三大特性:可见性、原子性和有序性。
可见性:
保证一个线程对变量的修改,其他线程能立即看到。比如,使用volatile关键字可以保证一个线程修改变量后,其他线程能立刻从主内存中读取到修改后的值。原子性:
保证对共享变量的操作是不可分割的,某个线程执行时,不会被其他线程打断。Java 的原子类(如AtomicInteger)就确保了对变量的原子操作。有序性:
保证指令的执行顺序不会被打乱,确保代码的执行顺序符合程序的设计逻辑。例如,
synchronized关键字能够确保同一时刻只有一个线程能够访问某个代码块,保证操作的顺序执行。happens-before 原则:JMM 定义了 happens-before 规则,用于约束操作之间的有序性。如果一个操作 A happens-before 操作 B,那么 A 的结果对于 B 是可见的,且 A 的执行顺序在 B 之前。
简而言之,JMM 就是一个为多线程提供规范和规则的机制,它保证了多个线程间的同步和内存一致性问题。
什么是 Java 中的原子性、可见性和有序性?
可见性:
- 保证一个线程对变量的修改,其他线程能立即看到。比如,使用 volatile 关键字可以保证一个线程修改变量后,其他线程能立刻从主内存中读取到修改后的值。
原子性:
- 保证对共享变量的操作是不可分割的,某个线程执行时,不会被其他线程打断。Java 的原子类(如 AtomicInteger)就确保了对变量的原子操作。
有序性:
- 保证指令的执行顺序不会被打乱,确保代码的执行顺序符合程序的设计逻辑。例如,synchronized 关键字能够确保同一时刻只有一个线程能够访问某个代码块,保证操作的顺序执行。
- happens-before 原则:JMM 定义了 happens-before 规则,用于约束操作之间的有序性。如果一个操作 A happens-before 操作 B,那么 A 的结果对于 B 是可见的,且 A 的执行顺序在 B 之前。
什么是 Java 的 happens-before 规则?
happens-before 是 JMM 中用于规定 “内存可见性与执行顺序” 的逻辑关系。
如果 A happens-before B,那么:
- A 的执行结果对 B 是可见的
- A 一定发生在 B 之前
happens-before 的 8 条规则(JSR-133 规定)
| 规则编号 | 描述 | 举例代码 |
| ———— | ————————————————————————————— | —————————————————————- |
| ① | 程序顺序规则:一个线程中语句按写的顺序执行 | int x=1; int y=x+1; |
| ② | 监视器规则:解锁 happens-before 随后的加锁(同一锁对象) | synchronized(obj) 块之间 |
| ③ | volatile 规则:volatile 写 happens-before 随后的读 | volatile boolean flag |
| ④ | 线程启动规则:start () happens-before 线程内任何操作 | main线程调用 t.start() |
| ⑤ | 线程终止规则:线程内操作 happens-before 其他线程检测它结束 | t.join() 或 !t.isAlive() |
| ⑥ | 线程中断规则:interrupt () happens-before 被检测到中断 | t.interrupt() 后用 isInterrupted() 检测 |
| ⑦ | 对象终结规则:构造完成 happens-before finalize () | Java GC 自动触发 |
| ⑧ | 传递性:A hb→B,B hb→C,则 A hb→C | A 设置值 → B 发信号 → C 读取所有结果 |

什么是 Java 中的指令重排?
指令重排是现代处理器和编译器用来优化代码执行的一种手段,它通过改变指令的执行顺序来提高程序的性能。
为什么要有指令重排这项优化呢?从 CPU 执行指令的原理来理解一下吧
为什么 CPU 要重排?
为了提高性能,现代 CPU 实现了以下机制:
- 指令流水线(Pipeline)
五级流水线:取指令 → 指令译码 → 执行 → 访存 → 写回
每条指令被分成多个阶段,这些阶段可以被多个指令并行执行。
这样做可以提高吞吐率(Throughput)。
- 超标量(SuperScalar)
现代 CPU 拥有多个执行单元(如整数、浮点、加载单元)
可以在一个时钟周期内执行多条指令,即 IPC > 1(Instruction per Clock)
指令重排的弊端
指令重排有时会导致并发程序中的问题,尤其是在多线程环境下。举个例子,假设有两个线程A和B,A执行某个任务后修改一个共享变量,B线程读取这个变量。如果没有适当的同步机制,由于指令重排,B可能会在A尚未修改变量时读取到一个不一致的值,这可能导致程序出现错误。
JMM 如何应对重排?
Java 内存模型(JMM)允许重排序,但通过 Happens-Before 规则和 volatile/synchronized 等机制来屏蔽 “坏的重排”。
扩展- 演示一下指令重排
- 诡异的结果
1 | int num = 0; |
你可能会觉得结果只有:
1:因为 ready == false
4:因为 ready == true 且 num = 2
但事实上,还可能出现一个诡异的结果:r1 == 0
- 为什么会出现 r1 == 0?
这是 指令重排(Instruction Reordering) 的锅:
actor2 看似顺序是:
1 | num = 2; |
但实际上,JIT 编译器或 CPU 为了性能可能重排序为:
1 | ready = true; |
- 线程交叉执行导致问题
设想线程切换如下:
| 时间 | 执行线程 | 执行内容 |
|---|---|---|
| T1 | actor2 | ready = true ✅ 重排了 |
| T2 | actor1 | if (ready) 进入 if 分支 |
| T3 | actor1 | 读取 num = 0,返回 0 |
| T4 | actor2 | num = 2 补上了 |
此时:
- ready = true
- num 还没来得及变成 2
- 所以 num + num = 0 + 0 = 0
- 用 JCStress 工具验证这种微小并发问题

- 如何解决?
- 方法一:使用 volatile
修改代码为:
1 | volatile boolean ready = false; |
效果:
- volatile 具有 可见性 和 禁止重排序 的语义
- 它会在 ready = true 写操作前,确保之前所有写操作(包括 num = 2)都已完成
- 执行压测后,0 就不再出现了,说明问题彻底解决。
总结:多线程环境下,不加 volatile 或同步机制,即使代码看起来顺序执行,也可能由于 “重排序” 导致结果诡异!
Java 中的 final 关键字是否能保证变量的可见性?
final 关键字确实能确保变量的值一旦被设置后不可再改变,但它不能保证变量在多个线程中的可见性。
- 对于基本数据类型,它只能保证值不变;
- 对于引用类型的 final 变量,它只能保证引用指向的对象不可改变,但是对象内部的数据,其他线程仍然可能看不到最新的值。
1 | public class YesFinalTest { |
在这个例子中,a 是 final 变量,它一旦赋值就不可更改。而 b 是普通变量,它是可以修改的。如果在多线程中对这些变量进行操作,final 并不能保证在其他线程中 a 和 b 的值能及时可见,除非使用 volatile 或其他同步机制。
为了确保线程安全和数据可见性。通常我们需要额外使用 volatile 或 synchronized 来确保数据的一致性和可见性。
为什么在 Java 中需要使用 ThreadLocal?
在 Java 中使用 ThreadLocal 主要是为了避免多线程环境下共享数据的安全问题。
通常,多个线程同时访问共享资源时,如果没有适当的同步机制,就会导致线程安全问题。而 ThreadLocal 通过为每个线程提供一个独立的副本,避免了这种多线程争用共享资源的情况。
ThreadLocal的好处如下:
- 避免竞争:ThreadLocal 是一种为每个线程提供独立副本的机制。每个线程在访问 ThreadLocal 变量时,会得到自己的副本,确保了数据隔离,避免了不同线程之间的竞争。
高效:因为每个线程都有自己的副本,线程之间不需要进行同步(如 synchronized),从而提高了效率,特别是在高并发情况下。
适用场景:ThreadLocal 非常适用于每个线程需要独立操作的数据。例如,数据库连接、会话信息等。使用 ThreadLocal 可以确保每个线程的数据独立,避免了共享资源带来的问题。
ThreadLocal会造成什么问题
ThreadLocal可能会造成内存泄漏的问题[https://www.mianshiya.com/bank/1789249312885223425/question/1780933295085744130]
Java 中的 ThreadLocal 是如何实现线程资源隔离的?
ThreadLocal 为每个线程提供了一个独立的 ThreadLocalMap,该 map 的键是 ThreadLocal 对象,值是存储在线程本地的具体数据。每个线程可以通过 ThreadLocal.get() 方法获取它自己的数据副本,其他线程无法直接访问到。
主要操作:
- set(value):将线程本地的值存入 ThreadLocalMap 中,键是 ThreadLocal 对象,值是具体的值。
- get():根据当前线程获取到该线程对应的 ThreadLocalMap,进而访问其中存储的值。
优点:
- 线程安全:每个线程都有自己的副本,线程之间互不干扰,不会发生竞争。
- 简化线程处理:避免了显式的同步操作,减少了线程间的复杂性。
使用场景:
- ThreadLocal 适用于每个线程需要持有独立副本的情况,例如:数据库连接、Session信息、用户上下文等。
为什么 Java 中的 ThreadLocal 对 key 的引用为弱引用?
在 Java 中,ThreadLocal 对于存储线程局部变量的 key 使用了弱引用。弱引用的主要作用是尽可能的避免内存泄漏,并允许 JVM 在内存紧张时回收不再使用的对象。
- 防止内存泄漏:每个线程都有一个与之关联的 ThreadLocalMap,ThreadLocal 中的 key 被弱引用,这意味着当一个线程结束或不再使用某个 ThreadLocal 对象时,ThreadLocalMap 可以被垃圾回收器回收,避免了内存泄漏。
- 有效管理内存:使用弱引用,当线程本地存储的对象不再被使用时,JVM 会自动回收内存。弱引用的 ThreadLocal 可以在系统内存不足时有效释放资源。
为什么value不设为弱引用?
我们不会把 value 也设置为弱引用,主要是为了保证线程能够稳定访问到自己的局部数据。如果 value 也使用弱引用的话,可能在垃圾回收时被回收掉,这样线程就无法正常访问自己的数据了。而且,ThreadLocal 本身就用弱引用管理 key,通过清理无效的 key 来避免内存泄漏,value 用强引用确保它在生命周期内都能被正确访问
Java 中使用 ThreadLocal 的最佳实践是什么?
在 Java 中使用 ThreadLocal 时,一些最佳实践包括:
- 避免滥用
ThreadLocal:ThreadLocal适用于每个线程需要维护独立变量的场景,比如数据库连接、用户会话等。如果需要共享数据,ThreadLocal不适用,应该使用其他线程安全的方式。 线程结束后清理:
使用ThreadLocal时,一定要在任务完成后清理数据。可以通过调用remove()方法确保释放资源,避免因资源未清理导致内存泄漏。使用合适的生命周期:
确保ThreadLocal只在当前线程中有效,线程结束时清理资源。避免 ThreadLocal 被长时间占用,最好在任务执行完后及时清理。合理使用
initialValue():
使用ThreadLocal时,可以通过withInitial()方法提供初始值,这样可以确保每个线程有默认的值,避免没有初始化的情况。避免静态存储大型对象:
ThreadLocal不应被用来存储大型对象,如数据库连接池等。因为每个线程都可能对这些数据进行修改,使用ThreadLocal存储这类资源并不合适,容易引发竞争条件。
总的来说,ThreadLocal 的最佳实践是:确保只在需要时使用,并且及时清理资源,避免内存泄漏或不必要的内存占用。
Java 中的 InheritableThreadLocal 是什么?
InheritableThreadLocal 就是 ThreadLocal 的一种变种,允许子线程访问和继承父线程中的本地变量。
一般来说,如果你需要在父线程和子线程之间共享一些数据,且希望子线程能够访问父线程设置的值,可以使用 InheritableThreadLocal。
它会在创建子线程时,将父线程中 InheritableThreadLocal 的值传递给子线程。需要注意的是,子线程的修改不会影响父线程的值,而且父线程的值也不会被子线程修改。
ThreadLocal 的缺点?
首先是内存泄漏的问题。ThreadLocal 对象在每个线程中有独立的副本,如果我们没有显式地使用 remove() 清除它们,这些线程局部的变量就不会被及时回收,可能会导致内存泄漏,尤其是在有大量线程的情况下。
其次是性能问题。如果我们使用 ThreadLocal 来存储大量数据,或者在一些线程池中使用 ThreadLocal 变量,可能会遇到性能瓶颈。这是因为每次获取或者设置值时,ThreadLocal 都需要访问其内部的数据结构,而这可能导致一些性能损耗,特别是当出现 hash 冲突时,性能会进一步下降(线性探测法效率低)。
最后,如果使用不当,ThreadLocal 会带来不必要的复杂性。例如,处理不同线程间的共享数据时,可能需要更多的管理工作,而 ThreadLocal 更适合一些轻量级的线程局部存储场景。”
什么是 Java 的 TransmittableThreadLocal?
TransmittableThreadLocal 是为了主要用于解决 ThreadLocal 在线程池中或跨线程的传递问题,它增强了 InheritableThreadLocal 的功能。通过捕获、重放、恢复三个步骤,它确保父线程的数据能够传递到子线程,从而避免了线程池中数据丢失的问题。
这就是为什么在涉及到线程池或多线程环境时,我们可以使用 TransmittableThreadLocal,它能够更好地保证线程间的数据共享和传递
ThreadLocal、InheritableThreadLocal 和 TransmittableThreadLocal 的简要比较
- ThreadLocal 用于确保每个线程有自己的变量,并且这些变量不能被其他线程访问。它适用于每个线程都需要独立存储的数据,比如数据库连接或用户信息。
- InheritableThreadLocal 让子线程能够继承父线程的 ThreadLocal 数据,这对于处理父子线程之间的数据传递是很有用的,但它不会在后续的子线程操作中进行更新。
- TransmittableThreadLocal 则扩展了 InheritableThreadLocal,支持跨线程池传递数据,它还可以在任务执行完成后清理数据,避免内存泄漏的问题。
Java 中 Thread.sleep 和 Thread.yield 的区别?
Thread.sleep() 和 Thread.yield() 都是用来控制线程执行行为的方法,但它们的底层机制和效果不同。
1.从线程状态上看:
- sleep() 会让线程进入 TIMED_WAITING(计时等待)状态,在指定时间内不会参与 CPU 调度。
- yield() 只是让当前线程从 RUNNABLE(可运行)状态 暂时让出 CPU 执行权,但不会阻塞,下一次调度时可能马上又被选中执行。
2.从 CPU 调度上看:
- sleep() 一定会让出 CPU 执行时间;
- yield() 只是“建议”调度器让出 CPU,是否让出、让给谁,由 JVM 和操作系统调度策略决定。
- 从异常机制上看:
- sleep() 会显式抛出 InterruptedException,需要捕获或声明;
- yield() 不会抛出任何异常。
4.从优先级角度看:
- sleep() 不考虑线程优先级,直接暂停;
- yield() 通常只会让给相同或更高优先级的线程。
5.共同点:
两者都不会释放已持有的锁。
(也就是说,如果在线程同步块里调用 sleep 或 yield,锁依旧被该线程占用。)
总结:
- sleep 是“强制暂停一段时间”;
- yield 是“自愿让出一次 CPU 机会”;
- sleep 会进入等待状态且抛异常,yield 只是回到就绪状态,不一定真的让出执行权。
Java 中 Thread.sleep(0) 的作用是什么?
Thread.sleep(0) 表示当前线程主动让出 CPU 的执行权,但不真正进入休眠。
它会触发一次线程调度,让系统有机会让其他同优先级或更高优先级的线程执行。
如果没有可运行的线程,调度器可能会重新选中自己执行。
可以理解为一种轻量级的线程切换,在某些系统上和 yield() 效果类似,但更“强制”一些。
sleep(0)的应用:
在高性能多线程程序中,有时会用 sleep(0) 做线程调度优化,比如防止一个线程长时间占用 CPU,但这属于底层微调,一般开发中用得较少。
sleep(0) 与 yield()的对比:
sleep(0) 与 yield() 的底层实现与调度策略不同:
sleep(0) 调用的是系统级 sleep/nanosleep,会强制触发一次调度;
yield() 只是告诉调度器“我可以让出”,但调度器可以忽略这个请求。
Java 中的 wait、notify 和 notifyAll 方法有什么作用?
这三个方法主要用于线程间的同步和通信,确保多线程之间不会相互干扰。
- 首先,wait() 会让当前线程进入等待状态,释放锁,让其他线程可以使用该锁。
- 调用 notify() 会唤醒一个在同一对象上等待的线程,
- 而 notifyAll() 会唤醒所有等待的线程。
这些方法都需要在 synchronized 块中使用,因为它们都涉及到共享资源的访问和修改。
简而言之,wait() 会让线程进入休眠并释放锁,notify() 唤醒一个等待的线程,而 notifyAll() 唤醒所有等待的线程。
Java 中什么情况会导致死锁?如何避免?
死锁就是两个或多个线程互相等待对方持有的锁,导致都无法继续执行。
它产生必须满足四个条件:互斥、占有且等待、不可抢占、循环等待。
只要破坏其中任意一个条件就能避免死锁。
实际开发中常见做法是:
保证加锁顺序一致;
对锁操作设置超时时间,避免无限等待;
尽量减小锁的粒度,降低死锁的风险;
必要时用 jstack 分析线程快照定位死锁。
补充:为什么“减小锁的粒度”有助于避免死锁?
先理解死锁的本质:
死锁发生的根源在于多个线程同时争抢多个锁资源,并且形成循环等待。
如果锁的粒度太大,意味着:
一个线程拿到的锁可能包含了多个关键资源;
另一个线程也需要访问其中部分资源时,也要等这把“大锁”;
多个线程间锁的依赖关系就复杂了,更容易形成循环等待链条。
相反,如果我们降低锁的粒度(即让每个锁控制的范围更小):
各线程锁住的资源重叠减少;
同时持有多个锁的概率变小;
就不太容易出现“我等你、你等我”的循环依赖。
结论:锁粒度小 → 资源竞争减少 → 同时持有多把锁的可能性低 → 死锁风险降低。
Java 中 volatile 关键字的作用是什么?
volatile可以用来修饰成员变量和静态成员变量,主要作用是保证变量的可见性和禁止指令重排优化。加了 volatile 之后线程不能从自己工作缓存中读取变量的值,必须去到主内存中获取变量的最新值。
volatile 与 synchronized 的可见性对比
| 特性 | volatile |
synchronized |
|---|---|---|
| 可见性 | ✅ 强制主内存交互 | ✅ 解锁前刷新主内存 |
| 原子性 | ❌ | ✅ 加锁保证互斥 |
| 性能 | 高 | 低(重量级锁) |
| 语义 | 轻量 | 阻塞 / 解阻塞 |
volatile 原理
volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)
- 对 volatile 变量的写指令后会加入写屏障
- 对 volatile 变量的读指令前会加入读屏障
| 类型 | 说明 |
|---|---|
| 写屏障 sfence | 保证之前的写操作会刷新到主内存 |
| 读屏障 lfence | 保证之后的读操作从主内存加载最新数据 |
如何保证可见性
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
1 | public void actor2(I_Result r) { |
- 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
1 | public void actor1(I_Result r) { |
通过 volatile ready,确保 actor2 的 num=2 先于 ready=true 被主内存可见;actor1 会在读 ready 之后拿到 num=2。

如何保证有序性(禁止指令重排序)
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
1 | public void actor2(I_Result r) { |
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
1 | public void actor1(I_Result r) { |

还是那句话,不能解决指令交错:
- 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
- 而有序性的保证也只是保证了本线程内相关代码不被重排序
即volatile无法阻止线程间指令交错执行,它只控制当前线程的读写顺序,无法强制让别的线程“看不见/先执行”
volatile无法阻止线程间指令交错执行,它只控制当前线程的读写顺序,无法强制让别的线程“看不见/先执行”与volatile禁止重排序是否矛盾?
不矛盾
| 层面 | 代表术语 | volatile 的作用 |
| ———————————— | ———————————— | ———————— |
| ✅ 当前线程内部顺序 | 指令重排序(reordering) | 禁止特定重排序 ✔️ |
| ❌ 多线程之间交叉执行 | 并发交叉 / 非同步执行 | 无法阻止 ❌ |



volatile 禁止 自身线程内的指令重排序,保证 “我写完作业再发信号”
volatile 无法阻止 线程之间的交错执行,不能保证 “别人不抢先读取信号”
两句话不矛盾,分别针对:
- 内部顺序保障(禁止重排)
- 外部同步保障(需要锁)
什么是 Java 中的 ABA 问题?
ABA 问题
- 问题:一个变量从 A → B → A,CAS 检查时发现值没变,但实际上经历了变化,从而导致错误的判断和操作
- 解决:使用版本号机制,在每次更新一个变量时,不仅更新变量的值,还更新一个版本号。如AtomicStampedReference,在 CAS 时同时比较值和版本号。
在 Java 中主线程如何知晓创建的子线程是否执行成功?
主线程通常有几种方式来知道子线程是否成功执行。总结来说,常见的方式包括:
1. 使用 Thread.join() 方法
- 原理:当主线程调用子线程的
join()方法时,它会阻塞等待直到子线程执行完毕。如果子线程正常执行结束,主线程会继续执行,否则如果子线程抛出异常,主线程可以捕获到这些异常。 - 应用场景:这种方式适合于简单的线程等待和执行结果的检查。
2. 使用 Callable 和 Future
- 原理:使用
Callable接口创建返回结果的任务,并通过Future.get()方法来获取子线程的执行结果。如果子线程执行成功,get()返回正常结果;如果抛出异常,get()会抛出相应的异常。 - 应用场景:这种方式更适合需要返回结果的任务,或者需要捕获子线程异常的场景。
3. 使用回调机制
- 原理:回调机制是通过主线程传递一个回调函数给子线程,子线程执行完任务后调用回调函数通知主线程。主线程可以通过这些回调函数来检查任务是否成功完成。
- 应用场景:适用于复杂的多线程场景,可以在任务结束时获取执行状态。
4. 使用 CountDownLatch 或其他 JUC 类
- 原理:通过
CountDownLatch等同步工具类,主线程可以等待子线程完成任务。当子线程完成任务时,会调用countDown()方法来通知主线程任务已完成。主线程通过await()方法等待直到所有子线程完成。 - 应用场景:适用于等待多个子线程的场景。
结合具体的业务需求选择合适的方案。例如,当只关心子线程是否完成任务时,可以使用 join();当需要获取任务结果或者捕获异常时,推荐使用 Callable 和 Future。
Java 创建线程池有哪些方式?
Java 创建线程池的方式
使用
Executors工具类- Java 提供了
Executors工具类来方便地创建线程池。最常见的是Executors.newFixedThreadPool(int n),它创建一个固定大小的线程池。线程池中有固定数量的线程来执行任务,一旦任务完成,线程就会被复用。 示例:
1
ExecutorService threadPool = Executors.newFixedThreadPool(10);
- Java 提供了
使用
ThreadPoolExecutor直接创建线程池- 如果需要更灵活的配置,可以使用
ThreadPoolExecutor类直接创建线程池。它可以让你自定义线程池的参数,如核心线程数、最大线程数、线程空闲保持时间等。 示例:
1
2
3
4
5
6
7ExecutorService threadPool = new ThreadPoolExecutor(
5, // corePoolSize
10, // maximumPoolSize
60, // keepAliveTime
TimeUnit.SECONDS, // TimeUnit
new LinkedBlockingQueue<>(100) // BlockingQueue
);
- 如果需要更灵活的配置,可以使用
使用
ForkJoinPool创建线程池ForkJoinPool用于并行任务的执行,它在任务拆分和合并方面非常高效。它特别适合需要递归任务或任务能拆分的场景。示例:
1
2
3
4ForkJoinPool forkJoinPool = new ForkJoinPool();
forkJoinPool.submit(() -> {
// Task
});
具体选择哪个方式取决于你的应用场景。
- 如果需要简单的线程池,可以用
Executors工具类; - 如果需要更高的定制性,选择
ThreadPoolExecutor; - 对于递归任务,
ForkJoinPool是一个很好的选择。
Java 线程安全的集合有哪些?
Java 线程安全的集合
常见的线程安全集合包括:
Vector
- 类型:线程安全的动态数组
- 特点:每个方法都有锁,适用于多线程修改的场景。但由于性能较差,已经不推荐使用。
Hashtable
- 类型:线程安全的哈希表
- 特点:每个方法都有锁,适用于多线程共享的哈希表,但性能较低,已被
ConcurrentHashMap替代。
ConcurrentHashMap
- 类型:线程安全的哈希表
- 特点:分段锁设计,支持高并发,适用于高并发环境,如缓存、分布式系统等。
CopyOnWriteArrayList
- 类型:线程安全的动态数组
- 特点:用于读多写少的场景,每次写操作时会复制整个数组,避免锁,适合监听集合等场景。
CopyOnWriteArraySet
- 类型:线程安全的集合
- 特点:基于
CopyOnWriteArrayList实现,适合读多写少的场景。
BlockingQueue
- 类型:线程安全的队列
- 特点:用于生产者-消费者模式,支持阻塞操作,适用于多线程的队列操作。
ConcurrentSkipListMap 和 ConcurrentSkipListSet
- 类型:线程安全的 Map 和 Set
- 特点:基于跳表实现,支持高并发操作,适合需要顺序访问的集合操作。
LinkedBlockingQueue
- 类型:线程安全的阻塞队列
- 特点:支持高并发的队列操作,适用于任务队列等场景。


