父子线程之间怎么共享_传递数据?

典型回答 当我们在同一个线程中,想要共享变量的话,是可以直接使用ThreadLocal的,但是如果在父子线程之间,共享变量,ThreadLocal就不行了。 如以下代码,会抛出NPE: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static ThreadLocal<Integer> sharedData = new ThreadLocal<>(); public static void main(String[] args) { sharedData.set(0); MyThread thread = new MyThread(); thread.start(); sharedData.set(sharedData.get() + 1); System.out.println("sharedData in main thread: " + sharedData.get()); } static class MyThread extends Thread { @Override public void run() { System.out.println("sharedData in child thread: " + sharedData.get()); sharedData.set(sharedData.get() + 1); System.out.println("sharedData in child thread after increment: " + sharedData.get()); } } 输出结果: ...

March 22, 2026 · 2 min · santu

CompletableFuture的底层是如何实现的?

典型回答 CompletableFuture是Java 8中引入的一个新特性,它提供了一种简单的方法来实现异步编程和任务组合。他的底层实现主要涉及到了几个重要的技术手段,如Completion链式异步处理、事件驱动、ForkJoinPool线程池、以及CountDownLatch控制计算状态、通过CompletionException捕获异常等。 CompletableFuture 内部采用了一种链式的结构来处理异步计算的结果,每个 CompletableFuture 都有一个与之关联的 Completion 链,它可以包含多个 Completion 阶段,每个阶段都代表一个异步操作,并且可以指定它所依赖的前一个阶段的计算结果。(在 CompletableFuture 类中,定义了一个内部类 Completion,它表示 Completion 链的一个阶段,其中包含了前一个阶段的计算结果、下一个阶段的计算操作以及执行计算操作的线程池等信息。) **CompletableFuture 还使用了一种事件驱动的机制来处理异步计算的完成事件。**在一个 CompletableFuture 对象上注册的 Completion 阶段完成后,它会触发一个完成事件,然后 CompletableFuture 对象会执行与之关联的下一个 Completion 阶段。 CompletableFuture 的异步计算是通过线程池来实现的。**CompletableFuture在内部使用了一个ForkJoinPool线程池来执行异步任务。**当我们创建一个CompletableFuture对象时,它会在内部创建一个任务,并提交到ForkJoinPool中去执行。 扩展知识 CompletableFuture应用场景 CompletableFuture可以用在多个场景中,比如我们有多线程编排的需求的话,可以使用CompletableFuture实现。 CompletableFuture非常适合实现异步计算,在异步计算过程中,可以将计算任务提交到线程池中,并等待计算结果。在计算完成后,可以将结果传递给下一个阶段,以继续进行计算。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; public class CompletableFutureDemo { public static void main(String[] args) { List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); List<CompletableFuture<Integer>> futures = nums.stream() .map(value -> CompletableFuture.supplyAsync(() -> { // 这里是每个异步任务要执行的操作, return value; })) .collect(Collectors.toList()); CompletableFuture<Integer> sumFuture = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApplyAsync(v -> { // 所有异步计算任务完成后,将它们的结果进行合并 int sum = futures.stream() .mapToInt(CompletableFuture::join) .sum(); return sum; }); int sum = sumFuture.join(); System.out.println(sum); } } 在上面的示例中,首先定义了一个包含 10 个整数的 List 对象 nums。接下来,使用 CompletableFuture.supplyAsync 方法将计算任务提交到线程池中进行并行计算。在使用join等待计算结果并输出。 ...

March 22, 2026 · 2 min · santu

三个线程分别顺序打印0-100

1 2 3 4 5 6 7 8 Thread-0: 0 Thread-1: 1 Thread-2: 2 Thread-0: 3 Thread-1: 4 Thread-2: 5 .... Thread-1: 100 典型回答 这个问题主要考察多线程的线程安全和通信机制,常见的处理方式有notify/synchorized和condition/ reentrantlock。但是往往有同学只注意线程安全,而忽略了通信机制,常见的错误写法如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Test { private static int i = 1; public static void main(String[] args) { for (int i = 0; i < 3; i++) { new Thread(new Print(i)).start(); } } private static class Print implements Runnable { private final int index; public Print(int index) { this.index = index; } @Override public void run() { while(true) { synchronized (Print.class) { if (i >= 101) { return; } System.out.println("Thread-" + index + " " + i++); } } } } } 这样写固然能通过锁来保证循环打印了1-100,但是却不能保证线程是按照顺序打印的,这个时候就需要用到线程的通信机制。 ...

March 22, 2026 · 4 min · santu

什么是总线嗅探和总线风暴,和JMM有什么关系?

典型回答 在Java内存模型中,总线嗅探和总线风暴问题与CPU缓存一致性有关。 在多线程编程中,如果多个线程共享同一个变量,且变量存储在主存中,则每个线程都有可能在自己的缓存中缓存该变量。如果一个线程修改了该变量的值,那么其他线程可能无法立即看到这个修改,因为它们缓存的是旧值。 为了保证缓存一致性,CPU 会使用总线嗅探机制来检测是否有其他处理器修改了该变量,如果有,则会将缓存中的旧值更新为新值。但是,如果多个线程频繁地读写共享变量,就会导致大量的总线通信,从而引发总线风暴的问题,降低系统的性能。 总线嗅探是多处理器系统中的一种通信机制,用于处理多个处理器共享的数据。在这种机制下,每个处理器都可以监视系统总线上的数据传输,以便了解数据是否与自己相关。如果数据与某个处理器相关,则该处理器将接管该数据,进行相应的操作。总线嗅探机制能够提高系统的性能,因为它能够减少数据冲突和锁竞争等问题,提高系统的并行性和效率。 总线嗅探也会引发总线风暴的问题。当多个处理器同时竞争总线上的资源时,就会产生大量的总线通信,从而导致总线风暴。总线风暴会降低系统的性能,并可能导致系统崩溃。 为了解决缓存一致性和总线风暴问题,Java内存模型提供了一系列同步机制,如synchronized、ReentrantLock等。这些机制能够保证线程之间的可见性和原子性,并通过锁竞争等方式减少总线通信,提高系统的性能和并发度。

March 22, 2026 · 1 min · santu

AQS是如何实现线程的等待和唤醒的?

典型回答 AQS(AbstractQueuedSynchronizer)是Java中实现锁和同步器的基础类,通过FIFO双向队列来管理等待线程和阻塞线程,实现线程之间的协作。 ✅如何理解AQS? AQS中线程等待和唤醒主要依赖park和unpark实现的。 当一个线程尝试获取锁或者同步器时,如果获取失败,AQS会将该线程封装成一个Node并添加到等待队列中,然后通过LockSupport.park()将该线程阻塞。 当一个线程释放锁或者同步器时,AQS会通过LockSupport.unpark()方法将等待队列中的第一个线程唤醒,并让其重新尝试获取锁或者同步器。 除了基本的等待和唤醒机制,AQS还提供了条件变量(Condition)的实现,用于在某些条件不满足时让线程等待,并在条件满足时唤醒线程。具体实现是通过创建一个等待队列,将等待的线程封装成Node并添加到队列中,然后将这些线程从同步队列中移除,并在条件满足时将等待队列中的所有线程唤醒。 扩展知识 park&unpark Java中的park()和unpark()方法是一对用于线程等待和唤醒的方法,一般用于实现锁、信号量、线程池等高级并发组件。 park()方法可以使调用线程进入休眠状态,等待被其他线程唤醒,具体实现会让线程进入等待队列中,等待被唤醒。park()方法可以通过传入一个Object类型的参数进行阻塞,这个参数是用来标识这个线程阻塞的原因,方便调试和排查问题。 unpark()方法可以使某个被阻塞的线程被唤醒,让其继续执行。unpark()方法需要传入一个Thread类型的参数,表示要唤醒的线程。

March 22, 2026 · 1 min · santu

有了InheritableThreadLocal为啥还需要TransmittableThreadLocal?

典型回答 InheritableThreadLocal是用于主子线程之间参数传递的,因为<font style="color:rgb(6, 10, 38);">I</font>nheritableThreadLocal的实现原理是,当创建一个新线程时,子线程会从父线程拷贝一份 **InheritableThreadLocal** 的值。这个拷贝发生在线程创建时(即 **Thread.init()** 阶段)。 但是,这种方式有一个问题,那就是必须要是在主线程中手动创建的子线程才可以,而现在池化技术非常普遍了,很多时候线程都是通过线程池进行创建和复用的,这时候InheritableThreadLocal就不行了。 因为线程池中的线程是预先创建好并复用的,任务提交时并不会创建新线程。因此,即使主线程设置了 InheritableThreadLocal 的值,在线程池中的工作线程早已创建完毕,不会再次执行“继承拷贝”逻辑。所以,在线程池中使用 **InheritableThreadLocal** 无法传递上下文。 TransmittableThreadLocal是阿里开源的一个方案 (开源地址:https://github.com/alibaba/transmittable-thread-local ) ,这个类继承并加强InheritableThreadLocal类。用来实现线程之间的参数传递,一经常被用在以下场景中: 分布式跟踪系统 或 全链路压测(即链路打标) 日志收集记录系统上下文 Session级Cache 应用容器或上层框架跨应用代码给下层SDK传递信息 TTL不是靠线程创建时继承,而是通过装饰任务(Runnable/Callable) 的方式,在任务提交到线程池前捕获当前线程的上下文,在任务执行时还原到工作线程,执行完后再清除。 使用 TtlRunnable.get(runnable) 或 TtlCallable.get(callable) 包装原始任务;在 run() 方法开始前,将捕获的上下文 set 到当前线程;执行完后 restore 原有状态。 因此,TTL 在线程池场景下也是有效的。 扩展知识 使用方式 先需要导入依赖: 1 2 3 4 5 <dependency> <groupId>com.alibaba</groupId> <artifactId>transmittable-thread-local</artifactId> <version>2.14.2</version> </dependency> 对于简单的父子线程之间参数传递,可以用以下方式: 1 2 3 4 5 6 7 TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>(); // 在父线程中设置 context.set("value-set-in-parent"); // 在子线程中可以读取,值是"value-set-in-parent" String value = context.get(); 如果在线程池中,可以用如下方式使用: ...

March 22, 2026 · 1 min · santu

Thread.sleep(0)的作用是什么?

典型回答 Thread的sleep会让线程暂时释放CPU资源,然后进入到TIMED_WAITING状态,等到指定时间之后会再尝试获取CPU时间片。 sleep方法需要指定一个时间,表示sleep的毫秒数,但是有的时候我们会见到Thread.sleep(0) 这种用法其实就是让当前线程释放一下CPU时间片,然后重新开始争抢。 这种用法一般比较少见,很多时候在一些底层框架中,可以用来做线程调度,比如某个线程长时间占用CPU资源,这时候通过sleep(0)让线程主动释放CPU时间片,让其他线程可以进行一次公平的争抢。 扩展知识 和yield的区别 Thread.yield()的主要作用是提示调度器,当前线程愿意让出 CPU,给其他同优先级或更高优先级的线程运行机会。但是它不保证当前线程一定会暂停,也不保证其他线程会立即运行。 Thread.sleep(0)其实也不保证一定能被其他线程获取到CPU时间片,但是他会先释放CPU,然后再参加竞争。这一点和yield有点区别。 Thread.sleep(0)是可以被中断的,如果被中断,会抛出异常InterruptedException,而Thread.yield()则无法被中断。 还有个小小差别,就是yield() 的实现是依赖于底层操作系统的线程调度策略。在某些系统上,它不保证一定能起作用(例如现代 Linux 的 CFS 调度器对 yield 支持有限)。

March 22, 2026 · 1 min · santu

如何保证多线程下 i++ 结果正确?

典型回答 想要保证多线程情况下,i++的正确性,需要考虑可见性、原子性及有序性。 在并发编程中,我们能用到的并发工具无非就是synchronized,volatile,reentrantLock以及并发工具类如AtomicInteger等。 这里面,除了volatile不可以以外(因为他没办法保证原子性),其他几种方式都可以。 ✅volatile能保证原子性吗?为什么? 使用 AtomicInteger 类: 1 2 3 4 5 private static AtomicInteger i = new AtomicInteger(0); public static void increment() { i.incrementAndGet(); } 使用synchronized: 1 2 3 4 5 6 7 8 9 public class HollisTest{ private static int i = 0; public void increment() { synchronized (HollisTest.class) { i++; } } } 使用reentrantLock: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class HollisTest { private int i = 0; private Lock lock = new ReentrantLock(); public void increment() { lock.lock(); try { i++; } finally { lock.unlock(); } } }

March 22, 2026 · 1 min · santu

有哪些实现线程安全的方案_

典型回答 在编程中,如果遇到并发安全的情况,有哪些方案可以来实现线程安全呢?以下是几个常见的方案(本文没有局限某种编程语言,而是说的整体思想): 1、单线程 想要实现线程安全,最简单的方式就是干脆不支持多线程,只用单线程来执行,那么就可以从根本上杜绝线程安全的问题了。比如Redis,就是这种思想,在命令执行时,只依赖单线程进行。 ✅Redis为什么被设计成是单线程的? 2、互斥锁 如果一定要用多线程,比较有效的方式就是排队,那么加锁是一种比较常见的排队方式,无论是synchronized、reentrantLock这种单机锁,还是Redis实现的分布式锁,还是数据库中的乐观锁、悲观锁,本地思想都是通过加互斥锁的方式让多个并发请求排队执行。 ✅synchronized和reentrantLock区别? 3、读写分离 除了加锁以外,还有一种做法,那就是读写分离,比如Java并发包中有一种COW机制,即写时复制,主要就是读和写作分离的思想,因为读操作并发是没什么影响的,而写操作的话,只需要让他不发生并发就行了。 比如,CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素add到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。 ✅什么是COW,如何保证的线程安全? 4、原子操作 原子操作是不可中断的操作,要么全部执行成功,要么全部失败。在多线程环境中,可以使用原子操作来实现对共享资源的安全访问,例如Java中的AtomicInteger等操作。 原子操作底层一般都是依赖的操作系统的CAS指令,思想也就是Compare And Swap ✅什么是CAS?存在什么问题? 5、不可变模式 并发问题之所以发生,有个重要原因就是因为有共享变量,试想一下,如果只有读的情况,那么永远也不会出现线程安全的问题,因为多线程读永远是线程安全的,但是多线程读写一定会存在线程安全的问题。 那既然这么说是不是通过只读就能解决并发问题呢?其实最简单的办法就是让共享变量只有读操作,而没有写操作。这个办法如此重要,以至于被上升到了一种解决并发问题的设计模式:不变性(Immutability)模式。 ✅什么是不可变模式,有哪些应用? 如Java中的String就是不可变模式的一种体现,他的好处就是永远不会出现并发问题。 ✅String的设计,用到了哪些设计模式? 6、数据不共享 像前面说的,如果没有共享数据,那么就不会有线程安全问题了,除了不可变模式,还有一种我们常用的手段来避免并发问题。那就是用ThreadLocal ✅什么是ThreadLocal,如何实现的?

March 22, 2026 · 1 min · santu

为什么JDK 15要废弃偏向锁?

典型回答 ✅synchronized的锁升级过程是怎样的? 在JDK 1.7中,引入了偏向锁的概念来优化synchronized的性能,但是偏向锁,在JDK 15中已经被废弃了。那么,为什么呢?(https://openjdk.org/jeps/374 ) JDK 15决定废弃偏向锁的主要原因是: 在过去,Java 应用通常使用的都是 HashTable、Vector 等比较老的集合库,这类集合库大量使用了 synchronized 来保证线程安全。所以偏向锁技术作为synchronized的一种优化手段,可以减少无锁竞争情况下的开销,通过假定一个锁一直由同一线程拥有,从而避免执行比较和交换的原子操作。 但是,偏向锁的局限是当只有一个线程反复进入同步代码块时他才能快速获得,但是当有其他线程尝试获取锁的时候,就需要等到 safe point 时,再将偏向锁撤销为无锁的状态或者升级为轻量级锁,而这个过程其实是会消耗一定的性能的。 ✅什么是safe point,有啥用? 在高并发的场景下,频繁的撤销偏向锁和重新偏向不仅不能提升性能,还会导致性能下降,特别是在那些锁竞争较为激烈的应用中。 并且,随着Java应用程序的发展和优化,过去能够从偏向锁中获得的性能提升在当今的应用中不再明显。许多现代应用程序使用了不需要同步的集合类或更高性能的并发数据结构(如ConcurrentHashMap、CopyOnWriteArrayList等),而不再频繁地执行无争用的同步(synchronized)操作。 还有就是官方在文档中提到的,偏向锁的引入导致代码很复杂,给HotSpot虚拟机中锁相关部分与其他组件之间的交互也带来了复杂性。这种复杂性使得理解代码的各个部分变得困难,并且阻碍了在同步子系统内进行重大设计更改。因此,废弃偏向锁有助于减少复杂性,使代码更容易维护和改进。 总之,废弃偏向锁是为了减少复杂性、提高代码可维护性,并鼓励开发人员采用更现代的并发编程技术,以适应当今Java应用程序的性能需求。

March 22, 2026 · 1 min · santu

留言给博主