为什么虚拟线程不能用synchronized?

典型回答 Java中的虚拟线程旨在提高并发编程的效率和性能,通过允许创建数以百万计的线程而对系统资源的消耗最小。虚拟线程背后的主要思想是将线程的调度任务从操作系统级别转移到Java虚拟机(JVM)级别,从而减少上下文切换的成本并提高系统的吞吐量。 ✅JDK21 中的虚拟线程是怎么回事? 虚拟线程是在JEP425中提出的,但是JEP425在介绍他的用法时,特意提到了一个比较关键的情况,那就是虚拟线程在两种情况下可能会被PINNED。(JDK 24中的 JEP 491开始,也不再PINNED了) 啥叫PINNED呢? “PINNED”(绑定)在虚拟线程的上下文中,指的是虚拟线程被绑定到一个特定的底层操作系统线程上,期间它不能被调度器移动到其他载体线程上执行。 虽然PINNED不会使应用程序运行不正确,但是他还是会带来一定的影响。 **虚拟线程的主要优势之一是它们能够在执行阻塞操作时被挂起,从而释放底层的操作系统线程给其他任务使用。**这种机制极大地提升了并发处理的能力和系统资源的利用率。当虚拟线程被PINNED时,它就会一直占用一个底层线程,即使执行阻塞操作也无法被挂起,从而限制了其他虚拟线程的执行和系统的整体可伸缩性。 而且,虚拟线程设计的一个核心目标是提高系统执行大量并发任务的能力,特别是I/O密集型任务。固定虚拟线程到底层线程意味着减少了虚拟机能够灵活调度和优化任务执行的能力,因此降低了资源利用效率。 所以,我们需要尽可能的避免PINNED的发生。 而根据JEP425的描述,执行synchronized块或方法时,会发生PINNED,synchronized关键字涉及获取和释放内部监视器锁,因为这种锁机制依赖于操作系统级别的同步原语,执行synchronized块或方法的虚拟线程需要固定到一个底层线程上,以保证锁的正确管理,直到完成。 另一个场景就是调用本地方法或外部函数的时候,这些操作通常涉及与操作系统或外部资源的直接交互,必须在特定的操作系统线程上执行。 所以,不建议在使用虚拟线程的时候使用synchronized,推荐使用java.util.concurrent包中提供的更高级的同步机制,如ReentrantLock、Semaphore等。这些机制提供了更细粒度的控制和更高的灵活性,使得开发者能够构建出既安全又高效的并发应用。 但是,这个问题,在25年3月18日 JDK 24推出之后,已经有所改变了。在 JEP 491 ****Synchronize Virtual Threads without Pinning中做了优化,不再进行绑定了。(https://openjdk.org/jeps/491)

March 22, 2026 · 1 min · santu

为什么虚拟线程不要和线程池一起用?

典型回答 在JEP425 中,关于虚拟线程的介绍中,多次提到,不要将虚拟线程池化。也就是说,不要把虚拟线程和线程池合着用。 主要原因是:像所有资源池一样,线程池旨在共享昂贵的资源,但虚拟线程并不昂贵,因此永远不需要将它们池化。 虚拟线程被设计为可以轻松地创建和销毁的轻量级实体,旨在允许每个任务都在其自己的虚拟线程中运行。这与传统的平台线程不同,后者创建和销毁的成本较高,因此经常被放入线程池中重用以提高效率。 虚拟线程的设计使得它们在遇到阻塞操作(如I/O操作)时可以被挂起,释放底层操作系统线程给其他任务使用。这种模型在高并发场景下能有效利用系统资源,减少了因阻塞操作而浪费的CPU周期。如果虚拟线程被放入池中重用,这种灵活的资源调度优势就会被削弱。 还有就是,虚拟线程的引入有个目的是为了简化并发编程模型,让开发者可以像编写顺序代码一样编写并发代码,而无需担心线程管理和线程池的复杂性。要求虚拟线程不被池化,是为了鼓励开发者利用这一简化的模型,避免回到传统的线程池管理模式。 总之,我们可以随意利用虚拟线程的轻量级特性和系统资源的高效利用,简化并发编程模型,而无需依赖传统的线程池技术。

March 22, 2026 · 1 min · santu

为什么虚拟线程尽量避免使用ThreadLocal

典型回答 在使用虚拟线程的时候,不建议使用ThreadLocal,这是JDK官网文档中提到的。 虚拟线程是支持ThreadLocal的,但由于可能创建的虚拟线程数量巨大,不当使用ThreadLocal可能导致内存泄漏等问题。如果需要,可以考虑使用作用域局部变量(Scope-local variables)作为替代方案。 Scoped Values,是JEP429中带来的一个特性,在JDK 21中已经推出了预览版。是Java中一种提供在特定执行范围内共享变量的机制,这种机制旨在为虚拟线程和结构化并发提供更好的支持。**与ThreadLocal变量不同,作用域局部变量不是绑定到线程上的,而是绑定到特定的执行范围或上下文。**这使得在使用虚拟线程时,管理跨任务共享的状态变得更加方便和安全。 与 Thread Local 不同的是,Scoped Value的值是不可变的。他的用法和ThreadLocal比较像: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class Server { final static ScopedValue<Principal> PRINCIPAL = ScopedValue.newInstance(); void serve(Request request, Response response) { var level = (request.isAdmin() ? ADMIN : GUEST); var principal = new Principal(level); ScopedValue.where(PRINCIPAL, principal) .run(() -> Application.handle(request, response)); } } class DBAccess { DBConnection open() { var principal = Server.PRINCIPAL.get(); if (!principal.canOpen()) throw new InvalidPrincipalException(); return newConnection(...); } } 因为这个特性目前还是预览版,并没有正式推出(截止24.3),所以暂时不做过多介绍了,后续推出正式版了,特性语法确定了再展开吧。

March 22, 2026 · 1 min · santu

什么是AQS的独占模式和共享模式?

典型回答 AbstractQueuedSynchronizer(AQS)是Java并发包中的一个核心框架,用于构建锁和其他同步组件。 ✅如何理解AQS? 它提供了一套基于FIFO队列的同步器框架,并支持独占模式和共享模式,这两种模式是用于实现同步组件的关键。 独占模式意味着一次只有一个线程可以获取同步状态。这种模式通常用于实现互斥锁,如ReentrantLock。 共享模式允许多个线程同时获取同步状态。这种模式通常用于实现如信号量(Semaphore)和读写锁(ReadWriteLock的读锁)等同步组件。 AQS的各种实现类中,要么是基于独占模式实现的, 要么是基于共享模式实现的。 举个例子。虽然不是很文雅,但是大家能懂。 1、独占模式:一次只能一个人用。其他人只能排队。就像是女厕所一样,每一个门只能进去一个人,进去一个人之后门就锁上了,其他人要等他出来之后再进。就像Java中的ReentrantLock 2、共享模式:只要还有名额,大家就都能用。就像是男厕所,他有小便池,只要有位置,就可以一起进来多个人,大家一起用。就像Java中的Semaphore 在AQS中提供了很多和锁操作相关的方法,如: tryAcquire、tryRelease、acquire、release等。 tryAcquireShared、tryReleaseShared、releaseShared、acquireShared等。 如果是独占模式,则需要实现tryAcquire、tryRelease、acquire、release等方法,如ReentrantLock: 如Semaphore则是实现了tryAcquireShared、tryReleaseShared等方法: 其中也有tryAcquire的实现,但是也是调用了tryAcquireShared来实现的: 在独占模式中,状态通常表示是否被锁定(0表示未锁定,1表示锁定)。在共享模式中,状态可以表示可用的资源数量。 当需要保证某个资源或一段代码在同一时间内只能被一个线程访问时,独占模式是最合适的选择。如我们经常用的ReentrantLock和ReadWriteLock中的写锁。 当资源或数据主要被多个线程读取,而写操作相对较少时,共享模式能够提高并发性能。如我们经常使用的Semaphore和CountDownLatch,用来多个线程控制共享资源的。还有ReadWriteLock中的读锁允许多个线程同时读取数据,只要没有线程在写入数据。

March 22, 2026 · 1 min · santu

什么是Java内存模型(JMM)?

典型回答 PS:正确叫法就是Java内存模型,而不是JVM内存模型,JVM内存结构可以回答堆栈方法区,Java内存模型就回答并发相关的,如果面试官问你JVM内存模型,那你要多问一句他问的是哪个。 Java程序是需要运行在Java虚拟机上面的,Java内存模型(Java Memory Model ,JMM)就是一种符合内存模型规范的,屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。 提到Java内存模型,一般指的是JDK 5 开始使用的新的内存模型,主要由JSR-133: JavaTM Memory Model and Thread Specification 描述(http://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf) Java内存模型规定了所有的共享变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。 而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。 JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。 扩展知识 计算机硬件升级带来的问题 多级缓存和一致性问题 ✅什么是操作系统的多级缓存 先看下上面关于多级缓存的介绍。 随着计算机能力不断提升,开始支持多线程。那么问题就来了。我们分别来分析下单线程、多线程在单核CPU、多核CPU中的影响。 单线程。 cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。 单核CPU,多线程。 进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。 多核CPU,多线程。 每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的cache中保留一份共享内存的缓存。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。 在CPU和主存之间增加缓存,在多线程场景下就可能存在缓存一致性问题,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。 CPU时间片与原子性问题 很多人都知道,现在我们用到操作系统,无论是Windows、Linux还是MacOS等其实都是多用户多任务分时操作系统。使用这些操作系统的“用户”是可以“同时”干多件事的,这已经是日常习惯了,并没觉得有什么特别。 但是实际上,对于单CPU的计算机来说,在CPU中,同一时间是只能干一件事儿的。 为了看起来像是“同时干多件事”,分时操作系统是把CPU的时间划分成长短基本相同的时间区间,即”时间片”,通过操作系统的管理,把这些时间片依次轮流地分配给各个“用户”使用。 如果某个“用户”在时间片结束之前,整个任务还没有完成,“用户”就必须进入到就绪状态,放弃CPU,等待下一轮循环。此时CPU又分配给另一个“用户”去使用。 CPU 就好像是一个电话亭,他可以开放给所有用户使用,但是他有规定,每个用户进入电话亭之后只能使用规定时长的时间。如果时间到了,用户还没打完电话,那就会被要求去重新排队。 不同的操作系统,在选择“用户”分配时间片的调度算法是不一样的,常用的有FCFS、轮转、SPN、SRT、HRRN、反馈等,由于不是本文重点,就不展开了。 这个电话亭可以允许哪个用户进入打电话是有不同的策略的,不同的电话亭规定不同,有的电话亭采用排队机制(FCFS)、有的优先分配给打电话时间最短的人(SPN)等 我们说原子性问题,其实指的是多线程场景中操作如果不能保证原子性,会导致处理结果和预期不一致。 前面我们提到过,线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。所以在多线程场景下,就会发生原子性问题。因为线程在执行一个读改写操作时,在执行完读改之后,时间片耗完,就会被要求放弃CPU,并等待重新调度。这种情况下,读改写就不是一个原子操作。 就好像我们去电话亭打电话,一共有三个步骤,查找电话,拨号,交流。由于我们在电话亭中可以停留的时间有限,有可能刚刚找到电话号码,时间到了,就被赶出来了。 在单线程中,一个读改写就算不是原子操作也没关系,因为只要这个线程再次被调度,这个操作总是可以执行完的。但是在多线程场景中可能就有问题了。因为多个线程可能会对同一个共享资源进行操作。 比如经典的 i++ 操作,对于一个简单的i++操作,一共有三个步骤:load , add ,save 。共享变量就会被多个线程同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,举个例子:如果i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2。 并发中的原子性和数据库中的原子性 原子性的概念是指:一个操作是不可中断的,要全部执行完成,要不就都不执行。 数据库事务中,保证原子性通过事务的提交和回滚,但是在并发编程中,是不涉及到回滚的。所以,并发编程中的原子性,强调的是一个操作的不可分割性。 所以,在并发编程中,原子性的定义不应该和事务中的原子性完全一样。他应该定义为:一段代码,或者一个变量的操作,在没有执行完之前,不能被其他线程执行。 指令重排与有序性问题 而且,我们知道,除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save 有可能被优化成load->save->add 。这就是有序性问题。 我们打电话的时候,除了可能被中途赶出来以外,本来正常步骤是要查找电话、拨号、交流的。但是电话亭非要给我们优化成查找电话、交流、拨号。这肯定不是我们想要的啊。 还是刚刚的i++操作,在满足了原子性的情况下,如果没有满足有序性,那么得到的结果可能也不是我们想要的。 ...

March 22, 2026 · 1 min · santu

什么是ThreadLocal,如何实现的?

典型回答 ThreadLocal是java.lang下面的一个类,是用来解决java多线程程序中并发问题的一种途径;通过为每一个线程创建一份共享变量的副本来保证各个线程之间的变量的访问和修改互相不影响; ThreadLocal存放的值是线程内共享的,线程间互斥的,主要用于线程内共享一些数据,避免通过参数来传递,这样处理后,能够优雅的解决一些实际问题。 比如一次用户的页面操作请求,我们可以在最开始的filter中,把用户的信息保存在ThreadLocal中,在同一次请求中,再使用到用户信息,就可以直接到ThreadLocal中获取就可以了。 ThreadLocal有四个方法,分别为: initialValue 返回此线程局部变量的初始值 get 返回此线程局部变量的当前线程副本中的值。如果这是线程第一次调用该方法,则创建并初始化此副本。 set 将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialValue() 方法来设置线程局部变量的值。 remove 移除此线程局部变量的值。 扩展知识 ThreadLocal的实现原理 ThreadLocal中用于保存线程的独有变量的数据结构是一个内部类:ThreadLocalMap,也是k-v结构。 key就是当前的ThreadLocal对象,而v就是我们想要保存的值。 上图中基本描述出了Thread、ThreadLocalMap以及ThreadLocal三者之间的包含关系。 **Thread类对象中维护了ThreadLocalMap成员变量,而ThreadLocalMap维护了以ThreadLocal为key,需要存储的数据为value的Entry数组。**这是它们三者之间的基本包含关系,我们需要进一步到源码中寻找踪迹。 查看Thread类,内部维护了两个变量,threadLocals和inheritableThreadLocals,它们的默认值是null,它们的类型是ThreadLocal.ThreadLocalMap,也就是ThreadLocal类的一个静态内部类ThreadLocalMap。 在静态内部类ThreadLocalMap维护一个数据结构类型为Entry的数组,节点类型如下代码所示: 1 2 3 4 5 6 7 8 9 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } 从源码中我们可以看到,Entry结构实际上是继承了一个ThreadLocal类型的弱引用并将其作为key,value为Object类型。这里使用弱引用是否会产生问题,我们这里暂时不讨论,在文章结束的时候一起讨论一下,暂且可以理解key就是ThreadLocal对象。对于ThreadLocalMap,我们一起来了解一下其内部的变量: 1 2 3 4 5 6 7 8 // 默认的数组初始化容量 private static final int INITIAL_CAPACITY = 16; // Entry数组,大小必须为2的幂 private Entry[] table; // 数组内部元素个数 private int size = 0; // 数组扩容阈值,默认为0,创建了ThreadLocalMap对象后会被重新设置 private int threshold; 这几个变量和HashMap中的变量十分类似,功能也类似。 ...

March 22, 2026 · 1 min · santu

什么是Unsafe?

典型回答 Unsafe是CAS的核心类。因为Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。 Unsafe是Java中一个底层类,包含了很多基础的操作,比如数组操作、对象操作、内存操作、CAS操作、线程(park)操作、栅栏(Fence)操作,JUC包、一些三方框架都使用Unsafe类来保证并发安全。 Unsafe类在jdk 源码的多个类中用到,这个类的提供了一些绕开JVM的更底层功能,基于它的实现可以提高效率。但是,它是一把双刃剑:正如它的名字所预示的那样,它是Unsafe的,它所分配的内存需要手动free(不被GC回收)。Unsafe类,提供了JNI某些功能的简单替代:确保高效性的同时,使事情变得更简单。 Unsafe类提供了硬件级别的原子操作,主要提供了以下功能: 1、通过Unsafe类可以分配内存,可以释放内存; 2、可以定位对象某字段的内存位置,也可以修改对象的字段值,即使它是私有的; 3、将线程进行挂起与恢复 4、CAS操作 扩展知识 被移除 Unsafe 在JDK 23中即将被移除(本文更新时 JDK23尚未发布正式版),主要是因为他本来就不是一个给开发者用的 API,而是为了给 JDK 自己用的,用它可以随意的处理堆内和堆外内存,非常不安全,所以要被移除了。 替代方案是JDK 9中的VarHandle和 JDK 22中的MemorySegment 举例 Unsafe 被设计的初衷,并不是希望被一般开发者调用,它的构造方法是私有的,所以我们不能通过 new 或者工厂方法去实例化 Unsafe 对象,通常可以采用反射的方法获取到 Unsafe 实例: 1 2 3 Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafeField.get(null); Unsafe中提供了一个静态的getUnsafe方法,可以返回一个unsafe的实例,但是这个只有在Bootstrap类加载器中可以使用,否则会抛出SecurityException 分配内存 unsafe中提供了allocateMemory方法来分配堆外内存,freeMemory方法来释放堆外内存。 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 32 33 34 35 36 import sun.misc.Unsafe; import java.lang.reflect.Field; public class UnsafeExample { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException { // 使用反射获取Unsafe实例 Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafeField.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafeField.get(null); // 分配堆外内存,返回内存地址 long size = 1024; // 内存大小 long address = unsafe.allocateMemory(size); // 写入数据到堆外内存 String dataToWrite = "Hello, this is hollis testing direct memory!"; byte[] dataBytes = dataToWrite.getBytes(); for (int i = 0; i < dataBytes.length; i++) { unsafe.putByte(address + i, dataBytes[i]); } // 从堆外内存读取数据 byte[] dataToRead = new byte[dataBytes.length]; for (int i = 0; i < dataBytes.length; i++) { dataToRead[i] = unsafe.getByte(address + i); } System.out.println(new String(dataToRead)); // 释放堆外内存 unsafe.freeMemory(address); } } 输出结果:Hello, this is hollis testing direct memory! CAS操作 使用Unsafe也可以实现一个CAS操作: ...

March 22, 2026 · 3 min · santu

什么是伪共享,如何解决伪共享?

典型回答 伪共享(False Sharing)是并发编程中一种性能问题,发生在多线程访问位于同一缓存行(cache line)中的不同变量时,导致缓存频繁失效和同步开销增大,从而严重影响程序性能。(概念看不懂没关系,继续看后面就懂了) 先看下这两篇文章,对于操作系统的CPU缓存和MESI有所了解后继续看。 ✅什么是操作系统的多级缓存 ✅什么是MESI缓存一致性协议 我们都知道,现代 CPU 使用缓存来弥补处理器与主内存之间的巨大速度差异。数据在主内存和 CPU 缓存(L1、L2、L3)之间传输的最小单位称为缓存行,一般是64字节。 为了保证多核 CPU 看到的内存视图是一致的,处理器使用如 MESI协议来协调各个核心的缓存状态。当一个核心修改了其缓存行中的数据时,该缓存行在其他核心中的副本会被标记为无效。其他核心后续访问该缓存行时需要重新从修改核心的缓存或主内存中加载最新数据。 那么,当多个线程在不同的 CPU 核心上运行时,它们就可能访问物理上相邻(位于同一个缓存行内)但逻辑上独立(被不同线程修改)的变量。 如果一个线程(线程1)修改了它自己关心的变量(变量 X),而这个变量恰好与另一个线程(线程2)关心的变量(变量 Y)位于同一个缓存行中。 由于缓存一致性协议是基于缓存行操作的,线程 A 对 X 的修改会导致整个缓存行在其他核心的缓存中失效。 当线程 B 需要访问它自己的变量 Y(即使 Y 本身没有被线程 A 修改)时,因为它所在的整个缓存行都失效了,线程 B 的 CPU 核心必须: ...

March 22, 2026 · 1 min · santu

什么是并发,什么是并行?

典型回答 并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。 那么,操作系统是如何实现这种并发的呢? 现在我们用到操作系统,无论是Windows、Linux还是MacOS等其实都是多用户多任务分时操作系统。使用这些操作系统的用户是可以“同时”干多件事的。 但是实际上,对于单CPU的计算机来说,在CPU中,同一时间是只能干一件事儿的。为了看起来像是“同时干多件事”,分时操作系统是把CPU的时间划分成长短基本相同的时间区间,即”时间片”,通过操作系统的管理,把这些时间片依次轮流地分配给各个用户使用。 如果某个作业在时间片结束之前,整个任务还没有完成,那么该作业就被暂停下来,放弃CPU,等待下一轮循环再继续做.此时CPU又分配给另一个作业去使用。 由于计算机的处理速度很快,只要时间片的间隔取得适当,那么一个用户作业从用完分配给它的一个时间片到获得下一个CPU时间片,中间有所”停顿”,但用户察觉不出来,好像整个系统全由它”独占”似的。 所以,在单CPU的计算机中,我们看起来“同时干多件事”,其实是通过CPU时间片技术,并发完成的。 提到并发,还有另外一个词容易和他混淆,那就是并行。 并发与并行之间的关系 并行(Parallel),当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。 Erlang 之父 Joe Armstrong 用一张比较形象的图解释了并发与并行的区别: 并发是两个队伍交替使用一台咖啡机。并行是两个队伍同时使用两台咖啡机。 映射到计算机系统中,上图中的咖啡机就是CPU,两个队伍指的就是两个进程。

March 22, 2026 · 1 min · santu

什么是线程池,如何实现的?

典型回答 线程池是池化技术的一种典型实现,所谓池化技术就是提前保存大量的资源,以备不时之需。在机器资源有限的情况下,使用池化技术可以大大的提高资源的利用率,提升性能等。 线程池,说的就是提前创建好一批线程,然后保存在线程池中,当有任务需要执行的时候,从线程池中选一个线程来执行任务。这样可以避免频繁创建和销毁线程的开销。 在编程领域,比较典型的池化技术有: 线程池、连接池、内存池、对象池等。 Java中线程池的继承关系如下: Java中的线程池都继承自ExecutorService这个接口,具体的实现上有两种线程池,分别加ThreadPoolExetutor和ForkJoinPool。本文主要讲的是ThreadPoolExetutor(本文代码基于JDK 21,旧版可能有略微差异,但是逻辑都一样),关于ForkJoinPool可以看: ✅ForkJoinPool和ThreadPoolExecutor区别是什么? 线程池的实现原理 不管是上面的哪个方法创建线程池,都是通过ThreadPoolExecutor的构造函数来创建的线程池(除ForkJoinPool类线程池外)。其实线程池是一个很复杂的结构,简单点说他有3部分组成。 对应到代码(ThreadPoolExecutor)中,就是以下这和几个: 任务队列(workQueue):是一个BlockingQueue,负责缓存待执行的任务。 工作线程集合(workers):是一个HashSet,负责管理所有工作线程的生命周期。 拒绝策略(handler):是一个RejectExecutionHandler,用来定义如何处理无法执行的任务。 Worker 这里还有个Worker的概念很重要,Worker是实现了 Runnable 接口的。每个 Worker 对象会包含一个任务和一个线程: 任务(Runnable firstTask):任务就是我们提交给线程池要执行的那个任务(Runnable类型),也就是说一个任务如果想被执行,都必须要变成一个Worker。 线程(Thread thread):Worker 会有一个线程来执行它的任务。这个线程是由 ThreadPoolExecutor 创建并管理的。 1 2 3 4 5 Worker(Runnable firstTask) { setState(-1); // inhibit interrupts until runWorker this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); } 每个 Worker 对象都会持有一个任务(firstTask)。当 Worker 被创建时,它会通过构造函数接收一个 Runnable 类型的任务。但是Worker并不是执行完这个任务就结束了,而是会继续从任务队列中取任务并执行,直到线程池关闭或任务队列为空。 Worker 中有一个 Thread 对象,它表示实际执行任务的工作线程。每个 Worker 都会拥有一个线程,线程会执行 run() 方法中的任务。 ...

March 22, 2026 · 2 min · santu

留言给博主