AQS为什么采用双向链表?

典型回答 AbstractQueuedSynchronizer (抽象队列同步器,以下简称 AQS)出现在 JDK 1.5 中。他是很多同步器的基础框架,比如 ReentrantLock、CountDownLatch 和 Semaphore 等都是基于 AQS 实现的。 ✅如何理解AQS? 上面这篇介绍过,在AQS内部,维护了一个FIFO队列和一个volatile的int类型的state变量。在state=1的时候表示当前对象锁已经被占有了,state变量的值修改的动作通过CAS来完成。 这个FIFO队列用来实现多线程的排队工作,他本质上是一个双向链表,因为他定义了两个Node,一个prev,一个next,这就是典型的双向链表。 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 abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { /** * Link to predecessor node that current node/thread relies on * for checking waitStatus. Assigned during enqueuing, and nulled * out (for sake of GC) only upon dequeuing. Also, upon * cancellation of a predecessor, we short-circuit while * finding a non-cancelled one, which will always exist * because the head node is never cancelled: A node becomes * head only as a result of successful acquire. A * cancelled thread never succeeds in acquiring, and a thread only * cancels itself, not any other node. */ volatile Node prev; /** * Link to the successor node that the current node/thread * unparks upon release. Assigned during enqueuing, adjusted * when bypassing cancelled predecessors, and nulled out (for * sake of GC) when dequeued. The enq operation does not * assign next field of a predecessor until after attachment, * so seeing a null next field does not necessarily mean that * node is at end of queue. However, if a next field appears * to be null, we can scan prev's from the tail to * double-check. The next field of cancelled nodes is set to * point to the node itself instead of null, to make life * easier for isOnSyncQueue. */ volatile Node next; } 在AQS的源码中,有一个图画了一下这个队列,但是他画的是单链表,但是其实现上是双链表的。 ...

March 22, 2026 · 7 min · santu

AQS的同步队列和条件队列原理?

典型回答 ✅如何理解AQS? 同步队列和条件队列是AQS中的两种不同队列,同步队列主要用于实现锁机制,而条件队列用于线程间的协调和通信。(本文代码为经典的JDK 1.8) 同步队列 同步队列主要用于实现锁的获取和释放。如我们常用的ReentrantLock,就是基于同步队列来实现的。 我们在介绍AQS的时候介绍过,它是一个FIFO队列,节点类型为AQS内部的Node类。当一个线程尝试获取锁失败时,它会被封装成一个Node节点加入到队列的尾部(每个节点(Node)代表一个等待的线程)。当锁被释放时,头节点的线程会被唤醒,尝试再次获取锁。 1 2 3 4 5 6 7 8 9 10 static final class Node { // 前驱和后继节点,构成双向链表 Node prev; Node next; // 线程本身 Thread thread; // 状态信息,表示节点在同步队列中的等待状态 int waitStatus; // ... } 同步的队列的实现原理比较简单: 当一个线程尝试获取锁并失败时,AQS会将该线程包装成一个节点(Node)并加入到队列的尾部。 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 private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); // 尝试快速路径:直接尝试在尾部插入节点 Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 快速路径失败时,进入完整的入队操作 enq(node); return node; } private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // 队列为空,初始化 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } 这个节点会处于等待状态,直到锁被其他线程释放。 当锁被释放时,头节点(持有锁的线程)会通知其后继节点(如果存在的话),后继节点尝试获取锁。 这个过程会一直持续,直到有线程成功获取锁或者队列为空。 条件队列 条件队列用于实现条件变量,允许线程在特定条件不满足时挂起,直到其他线程改变了条件并显式唤醒等待在该条件上的线程。比较典型的一个条件队列的使用场景就是ReentrantLock的Condition。 ...

March 22, 2026 · 2 min · santu

ForkJoinPool和ThreadPoolExecutor区别是什么?

典型回答 ForkJoinPool和ExecutorService都是Java中常用的线程池的实现,他们主要在实现方式上有一定的区别,所以也就会同时带来的适用场景上面的区别。 首先在**实现方式**上,ForkJoinPool 是基于工作窃取(Work-Stealing)算法实现的线程池,ForkJoinPool 中每个线程都有自己的工作队列,用于存储待执行的任务。当一个线程执行完自己的任务之后,会从其他线程的工作队列中窃取任务执行,以此来实现任务的动态均衡和线程的利用率最大化。 ThreadPoolExecutor 是基于任务分配(Task-Assignment)算法实现的线程池,ThreadPoolExecutor 中线程池中有一个共享的工作队列,所有任务都将提交到这个队列中。线程池中的线程会从队列中获取任务执行,如果队列为空,则线程会等待,直到队列中有任务为止。 ForkJoinPool 中的任务通常是一些可以分割成多个子任务的任务,例如快速排序。每个任务都可以分成两个或多个子任务,然后由不同的线程来执行这些子任务。在这个过程中,ForkJoinPool 会自动管理任务的执行、分割和合并,从而实现任务的动态分配和最优化执行。 ForkJoinPool 中的工作线程是一种特殊的线程,与普通线程池中的工作线程有所不同。它们会自动地创建和销毁,以及自动地管理线程的数量和调度。这种方式可以降低线程池的管理成本,提高线程的利用率和并行度。 ThreadPoolExecutor 中线程的创建和销毁是静态的,线程池创建后会预先创建一定数量的线程,根据任务的数量动态调整线程的利用率,不会销毁线程。如果线程长时间处于空闲状态,可能会占用过多的资源。 在**使用场景**上也有区别,ThreadPoolExecutor 适合处理 IO 密集型或普通 CPU 任务,如网络请求处理、数据库访问、Web 服务请求调度。尤其是大量独立、不需要拆分的小任务。 ForkJoinPool** 适合于 CPU 密集型、可拆分的并行计算任务,** 大任务分解为小任务:适用于可以递归分解为更小任务的大型任务。ForkJoinPool 通过分而治之的方式,将大任务拆分为小任务,这些小任务可以并行处理。 计算密集型任务:对于需要大量计算且能够并行化的任务,ForkJoinPool 是一个理想的选择。它能够有效利用多核处理器的优势来加速处理过程。 递归算法的并行化:适合于可以用递归方法解决的问题,如快速排序、归并排序、图像处理中的分区算法等。 数据聚合任务:在处理需要聚合多个数据源结果的任务时(例如,遍历树结构并聚合结果),ForkJoinPool 提供了有效的方式来并行化这一过程。 扩展知识 为什么CompletableFuture使用ForkJoinPool CompletableFuture 使用 ForkJoinPool 而不是 ExecutorService 的原因主要是因为它的执行模型和任务分割方式与 ForkJoinPool 更加匹配。 在 CompletableFuture 中,一个任务可以分割成多个子任务,并且这些子任务之间可以存在依赖关系。而ForkJoinPool 本身就是一种支持任务分割和合并的线程池实现,能够自动地处理任务的拆分和合并。而且,ForkJoinPool 还有一种工作窃取算法,能够自动地调整线程的负载,提高线程的利用率和并行度。 **ForkJoinPool 还有一个特点,就是它的线程池大小是动态调整的。**当任务比较少时,线程池的大小会自动缩小,从而减少了线程的数量和占用的系统资源。当任务比较多时,线程池的大小会自动增加,从而保证任务能够及时地得到执行。 如果使用 ExecutorService 来执行这些任务,需要手动地创建线程池、任务队列和任务执行策略,并且需要手动地处理任务的拆分和合并,实现起来相对比较复杂。 因此,ForkJoinPool 更加适合 CompletableFuture 的执行模型。 ForkJoinPool使用示例 下面是一个使用 ForkJoinPool 实现快排的代码: 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import java.util.concurrent.RecursiveAction; import java.util.concurrent.ForkJoinPool; public class ParallelQuickSort extends RecursiveAction { private int[] array; private int left; private int right; public ParallelQuickSort(int[] array, int left, int right) { this.array = array; this.left = left; this.right = right; } private int partition(int left, int right) { int pivot = array[right]; int i = left - 1; for (int j = left; j < right; j++) { if (array[j] <= pivot) { i++; // Swap array[i] and array[j] int temp = array[i]; array[i] = array[j]; array[j] = temp; } } // Swap array[i+1] and array[right] (or pivot) int temp = array[i + 1]; array[i + 1] = array[right]; array[right] = temp; return i + 1; } @Override protected void compute() { if (left < right) { int partitionIndex = partition(left, right); // Parallelize the two subtasks ParallelQuickSort leftTask = new ParallelQuickSort(array, left, partitionIndex - 1); ParallelQuickSort rightTask = new ParallelQuickSort(array, partitionIndex + 1, right); leftTask.fork(); rightTask.fork(); leftTask.join(); rightTask.join(); } } public static void parallelQuickSort(int[] array) { ForkJoinPool pool = new ForkJoinPool(); pool.invoke(new ParallelQuickSort(array, 0, array.length - 1)); } public static void main(String[] args) { int[] array = { 12, 35, 87, 26, 9, 28, 7 }; parallelQuickSort(array); for (int i : array) { System.out.print(i + " "); } } } ParallelQuickSort 类继承自 RecursiveAction。在这个类中,compute 方法实现了快速排序的逻辑,包括分区(partition 方法)和递归调用。对于每个递归调用,它创建了一个新的 ParallelQuickSort 实例,并通过 fork 方法将其提交给 ForkJoinPool 以异步执行。 ...

March 22, 2026 · 2 min · santu

JDK21 中的虚拟线程是怎么回事?

典型回答 虚拟线程这个名字很多人可能比较懵,但是如果对像Go、Ruby、python等语言有一些了解的话,就会很快的反应过来,其实这就是协程。 在以前的JDK中,Java的线程模型其实比较简单,在大多数操作系统中,主要采用的是基于轻量级进程实现的一对一的线程模型,简单来说就是每一个Java线程对应一个操作系统中的轻量级进程,这种线程模型中的线程创建、析构及同步等动作,都需要进行系统调用。而系统调用则需要在用户态(User Mode)和内核态(Kernel Mode)中来回切换,所以性能开销还是很大的。 而新引入的虚拟线程,是JDK 实现的轻量级线程,他可以避免上下文切换带来的的额外耗费。他的实现原理其实是JDK不再是每一个线程都一对一的对应一个操作系统的线程了,而是会将多个虚拟线程映射到少量操作系统线程中,通过有效的调度来避免那些上下文切换。 在JDK 21,有多种方法可以创建协程,如Thread.startVirtualThread()、Executors.newVirtualThreadPerTaskExecutor()等。 扩展知识 线程的实现方式 在操作系统中,线程是比进程更轻量级的调度执行单位,线程的引入可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源,又可以独立调度。 其实,线程的实现方式主要有三种:分别是使用内核线程实现、使用用户线程实现以及使用用户线程加轻量级进程混合实现。 ✅线程的实现方式有哪些? Java的线程实现 以上讲的是操作系统的线程的实现的三种方式,不同的操作系统在实现线程的时候会采用不同的机制,比如windows采用的是内核线程实现的,而Solaris则是通过混合模式实现的。 而Java作为一门跨平台的编程语言,实际上他的线程的实现其实是依赖具体的操作系统的。而比较常用的windows和linux来说,都是采用的内核线程的方式实现的。 也就是说,当我们在JAVA代码中创建一个Thread的时候,其实是需要映射到操作系统的线程的具体实现的,因为常见的通过内核线程实现的方式在创建、调度时都需要进行内核参与,所以成本比较高,尽管JAVA中提供了线程池的方式来避免重复创建线程,但是依旧有很大的优化空间。而且这种实现方式意味着受机器资源的影响,平台线程数也是有限制的。 虚拟线程 **JDK 21引入的虚拟线程,是JDK 实现的轻量级线程,他可以避免上下文切换带来的额外耗费。**他的实现原理其实是JDK不再是每一个线程都一对一的对应一个操作系统的线程了,而是会将多个虚拟线程映射到少量操作系统线程中,通过有效的调度来避免那些上下文切换。 而且,我们可以在应用程序中创建非常多的虚拟线程,而不依赖于平台线程的数量。这些虚拟线程是由JVM管理的,因此它们不会增加额外的上下文切换开销,因为它们作为普通Java对象存储在RAM中。 虚拟线程和平台线程的区别 首先,虚拟线程总是守护线程。setDaemon (false)方法不能将虚拟线程更改为非守护线程。所以,需要注意的是,当所有启动的非守护线程都终止时,JVM将终止。这意味着JVM不会等待虚拟线程完成后才退出。 其次,即使使用setPriority()方法,虚拟线程始终具有normal的优先级,且不能更改优先级。在虚拟线程上调用此方法没有效果。 还有就是,虚拟线程是不支持stop()、suspend()或resume()等方法。这些方法在虚拟线程上调用时会抛出UnsupportedOperationException异常。 如何使用 接下来介绍一下,在JDK 21中如何使用虚拟线程。 首先,通过Thread.startVirtualThread()可以运行一个虚拟线程: 1 2 3 Thread.startVirtualThread(() -> { System.out.println("虚拟线程执行中..."); }); 其次,通过Thread.Builder也可以创建虚拟线程,Thread类提供了ofPlatform()来创建一个平台线程、ofVirtual()来创建虚拟线程。 1 2 3 4 5 Thread.Builder platformBuilder = Thread.ofPlatform().name("平台线程"); Thread.Builder virtualBuilder = Thread.ofVirtual().name("虚拟线程"); Thread t1 = platformBuilder .start(() -> {...}); Thread t2 = virtualBuilder.start(() -> {...}); 另外,线程池也支持了虚拟线程,可以通过Executors.newVirtualThreadPerTaskExecutor()来创建虚拟线程: ...

March 22, 2026 · 2 min · santu

synchronized 的锁能降级吗?

典型回答 ✅synchronized的锁升级过程是怎样的? 我们知道,synchronized 是有锁升级的过程的,会从偏向锁升级到轻量级锁和重量级锁,那么synchronized的锁有降级的过程吗?(这个问题,网上也有很多文章,五花八门。。。) 大家理解的锁降级,如果是指锁从重量级状态回退到轻量级或偏向锁状态的过程,那么可以明确的说,当前的HotSpot虚拟机实现是不支持的。 因为锁一旦升级为重量级锁,它将保持在这个状态,直到锁被完全释放。 但是,你要说,一旦一个锁从偏向、到轻量级锁、再到重量级锁加锁之后,后面的所有加锁都是以重量级锁的方式加锁了,这么说也不对! **因为有一种特殊情况的"降级",那就是重量级锁的Monitor对象在不再被任何线程持有时,被清理和回收的过程。**这一过程确实可以在Stop-the-World(STW)暂停期间进行,这时所有Java线程都停在安全点(SafePoint)。这个过程会做以下事情: 锁状态检查:在STW停顿期间,JVM会检查所有的Monitor对象。 确定降级对象:JVM识别出那些没有被任何线程持有的Monitor对象。这通常是通过检查Monitor对象的锁计数器或者所有权信息来实现的。 “降级"操作:对于那些确定未被使用的Monitor对象,JVM会进行所谓的“deflation”操作,即清理这些对象的状态,使其不再占用系统资源。在某些情况下,这可能涉及到重置Monitor状态,释放与其相关的系统资源等。 以上,说的是 HotSpot,并不是所有虚拟机都这样,有的虚拟机还真支持从重量级锁降级到轻量级锁,比如 JRocket 这个虚拟机。 下面是关于JRocket 的锁的降级的说明( https://docs.oracle.com/cd/E13188_01/jrockit/docs142/usingJRA/applocks.html) 翻译一下就是,当最后一个争用线程释放重量级锁时,锁通常仍然保持为重量级。即使没有争用,获取重量级锁的代价也比获取轻量级锁(thin lock)更高。如果JRockit认为锁会从变轻中受益,它可能会再次将其“压缩”为轻量级锁。

March 22, 2026 · 1 min · santu

synchronized是怎么实现的?

典型回答 synchronized 是 Java 中的一个很重要的关键字,主要用来加锁,synchronized 所添加的锁有以下几个特点。synchronized 的使用方法比较简单,主要可以用来修饰方法和代码块。根据其锁定的对象不同,可以用来定义同步方法和同步代码块。 方法级的同步是隐式的(同步方法)。同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。当某个线程要访问某个方法的时候,会检查是否有 ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。 同步代码块使用 monitorenter 和 monitorexit 两个指令实现。 可以把执行 monitorenter 指令理解为加锁,执行 monitorexit 理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0,当一个线程获得锁(执行 monitorenter )后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行 monitorexit 指令)的时候,计数器再自减。当计数器为 0 的时候。锁将被释放,其他线程便可以获得锁。 扩展知识 synchronized synchronized 是 Java 中的一个很重要的关键字,主要用来加锁,synchronized 所添加的锁有以下几个特点。 互斥性 同一时间点,只有一个线程可以获得锁,获得锁的线程才可以处理被 synchronized 修饰的代码片段。 阻塞性 只有获得锁的线程才可以执行被 synchronized 修饰的代码片段,未获得锁的线程只能阻塞,等待锁释放。 可重入性 如果一个线程已经获得锁,在锁未释放之前,再次请求锁的时候,是必然可以获得锁的。 synchronized 的用法 synchronized 的使用方法比较简单,主要可以用来修饰方法和代码块。根据其锁定的对象不同,可以用来定义同步方法和同步代码块。 同步方法 1 2 3 4 5 6 7 8 9 //同步方法,对象锁 public synchronized void doSth(){ System.out.println("Hello World"); } //同步方法,类锁 public synchronized static void doSth(){ System.out.println("Hello World"); } 以上代码,在方法的作用域(public)后面增加 Synchronized,即可声明一个同步方法。 ...

March 22, 2026 · 4 min · santu

synchronized的锁优化是怎样的?

典型回答 高效并发是从JDK 1.5 到 JDK 1.6的一个重要改进,HotSpot虚拟机开发团队在这个版本中花费了很大的精力去对Java中的锁进行优化,如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等。这些技术都是为了在线程之间更高效的共享数据,以及解决竞争问题。 关于轻量级锁和偏向锁,还有适应性自旋参考: https://www.yuque.com/hollis666/ec96i7/cv5kt1 https://www.yuque.com/hollis666/ec96i7/dc6vfx4nfvptib2y 本文,主要先来介绍一下自旋、锁消除以及锁粗化等技术。 自旋锁 我们介绍的synchronized的实现方式时,说过,它是使用Monitor进行加锁,这是一种互斥锁,为了表示他对性能的影响我们称之为重量级锁。 这种互斥锁在互斥同步上对性能的影响很大,Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到内核态,因此状态转换需要花费很多的处理器时间。 就像去银行办业务的例子,当你来到银行,发现柜台前面都有人的时候,你需要取一个号,然后再去等待区等待,一直等待被叫号。这个过程是比较浪费时间的,那么有没有什么办法改进呢? 有一种比较好的设计,那就是银行提供自动取款机,当你去银行取款的时候,你不需要取号,不需要去休息区等待叫号,你只需要找到一台取款机,排在其他人后面等待取款就行了。 之所以能这样做,是因为取款的这个过程相比较之下是比较节省时间的。如果所有人去银行都只取款,或者办理业务的时间都很短的话,那也就可以不需要取号,不需要去单独的休息区,不需要听叫号,也不需要再跑到对应的柜台了。 而,在程序中,Java虚拟机的开发工程师们在分析过大量数据后发现:共享数据的锁定状态一般只会持续很短的一段时间,为了这段时间去挂起和恢复线程其实并不值得。 如果物理机上有多个处理器,可以让多个线程同时执行的话。我们就可以让后面来的线程“稍微等一下”,但是并不放弃处理器的执行时间,看看持有锁的线程会不会很快释放锁。这个“稍微等一下”的过程就是自旋。 自旋锁在JDK 1.4中已经引入,在JDK 1.6中默认开启。 很多人在对于自旋锁的概念不清楚的时候可能会有以下疑问:这么听上去,自旋锁好像和阻塞锁没啥区别,反正都是等着嘛。 对于去银行取钱的你来说,站在取款机面前等待和去休息区等待叫号有一个很大的区别: 那就是如果你在休息区等待,这段时间你什么都不需要管,随意做自己的事情,等着被唤醒就行了。 如果你在取款机面前等待,那么你需要时刻关注自己前面还有没有人,因为没人会唤醒你。 很明显,这种直接去取款机前面排队取款的效率是比较高。 所以呢,自旋锁和阻塞锁最大的区别就是,到底要不要放弃处理器的执行时间。对于阻塞锁和自旋锁来说,都是要等待获得共享资源。但是阻塞锁是放弃了CPU时间,进入了等待区,等待被唤醒。而自旋锁是一直“自旋”在那里,时刻的检查共享资源是否可以被访问。 由于自旋锁只是将当前线程不停地执行循环体,不进行线程状态的改变,所以响应速度更快。但当线程数不停增加时,性能下降明显,因为每个线程都需要执行,占用CPU时间。如果线程竞争不激烈,并且保持锁的时间短。适合使用自旋锁。 锁消除 除了自旋锁之后,JDK中还有一种锁的优化被称之为锁消除。还拿去银行取钱的例子说。 你去银行取钱,所有情况下都需要取号,并且等待吗?其实是不用的,当银行办理业务的人不多的时候,可能根本不需要取号,直接走到柜台前面办理业务就好了。 能这么做的前提是,没有人和你抢着办业务。 上面的这种例子,在锁优化中被称作“锁消除”,是JIT编译器对内部锁的具体实现所做的一种优化。 在动态编译同步块的时候,JIT编译器可以借助一种被称为逃逸分析(Escape Analysis)的技术来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。 如果同步块所使用的锁对象通过这种分析被证实只能够被一个线程访问,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。 如以下代码: 1 2 3 4 5 6 public void f() { Object hollis = new Object(); synchronized(hollis) { System.out.println(hollis); } } 代码中对hollis这个对象进行加锁,但是hollis对象的生命周期只在f()方法中,并不会被其他线程所访问到,所以在JIT编译阶段就会被优化掉。优化成: 1 2 3 4 public void f() { Object hollis = new Object(); System.out.println(hollis); } 这里,可能有读者会质疑了,代码是程序员自己写的,程序员难道没有能力判断要不要加锁吗?就像以上代码,完全没必要加锁,有经验的开发者一眼就能看的出来的。其实道理是这样,但是还是有可能有疏忽,比如我们经常在代码中使用StringBuffer作为局部变量,而StringBuffer中的append是线程安全的,有synchronized修饰的,这种情况开发者可能会忽略。这时候,JIT就可以帮忙优化,进行锁消除。 ...

March 22, 2026 · 1 min · santu

synchronized的锁升级过程是怎样的?

典型回答 在JDK 1.6及之前的版本中,synchronized锁是通过对象内部的一个叫做监视器锁(也称对象锁)来实现的。当一个线程请求对象锁时,如果该对象没有被锁住,线程就会获取锁并继续执行。如果该对象已经被锁住,线程就会进入阻塞状态,直到锁被释放。这种锁的实现方式称为**“重量级锁”**,因为获取锁和释放锁都需要在操作系统层面上进行线程的阻塞和唤醒,而这些操作会带来很大的开销。 在JDK 1.6之后,synchronized锁的实现发生了一些变化,引入了“偏向锁”、“轻量级锁”和“重量级锁”三种不同的状态,用来适应不同场景下的锁竞争情况。 在JDK 15 中,废弃了偏向锁(https://openjdk.org/jeps/374 ) 所以,在Java中,锁的状态分为四种,分别是无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。在Java中,mark word的低两位用于表示锁的状态,分别为“01”(无锁状态)、“01”(偏向锁状态)、“00”(轻量级锁状态)和“10”(重量级锁状态)。但是由于无锁状态和偏向锁都是"01",所以在低三位引入偏向锁标记位,用"0"表示无锁,“1"表示偏向。 以下偏向过程基于OpenJDK 8 源码总结的,源码地址:https://github.com/openjdk/jdk8u/blob/master/hotspot/src/share/vm/runtime/synchronizer.cpp 具体的锁升级过程如下,这里为了方便大家理解,暂时不包含自旋相关的内容 无锁 当一个线程第一次访问一个对象的同步块时,JVM会在对象头中设置该线程的Thread ID,并将对象头的状态位设置为“偏向锁”。这个过程称为“偏向”,表示对象当前偏向于第一个访问它的线程。 偏向锁(Biased Locking) 当一个synchronized块被线程首次进入时,锁对象会进入偏向模式。 在偏向锁模式下,锁会偏向于第一个获取它的线程,JVM 会在对象头中记录该线程的 ID 作为偏向锁的持有者,并将对象头中的 Mark Word 中的一部分作为偏向锁标识。 在这种情况下,如果其他线程访问该对象,会先检查该对象的偏向锁标识,如果和自己的线程 ID 相同,则直接获取锁。如果不同,则该对象的锁状态就会升级到轻量级锁状态。 触发条件:首次进入synchronized块时自动开启,假设JVM启动参数没有禁用偏向锁。 但是需要注意,在JDK 15中,偏向锁已被废除: ✅为什么JDK 15要废弃偏向锁? 轻量级锁(Lightweight Locking) 当有另一个线程尝试获取已被偏向的锁时,偏向锁会被撤销,锁会升级为轻量级锁。 在轻量级锁状态中,JVM 为对象头中的 Mark Word 预留了一部分空间,用于存储指向线程栈中锁记录的指针。 当一个线程尝试获取轻量级锁时,JVM的做法是: 将对象头中的Mark Word复制到线程栈中的锁记录(Lock Record):每个Java对象头部都有一个Mark Word,它用于存储对象自身的运行时数据,如哈希码、锁状态信息、代年龄等。当线程尝试获取轻量级锁时,JVM会在当前线程的栈帧中创建一个锁记录空间,然后将对象头中的Mark Word复制到这个锁记录中。这个复制的Mark Word被称为“Displaced Mark Word”。 尝试通过CAS操作更新对象头的Mark Word:接下来,JVM尝试使用CAS(Compare-And-Swap)操作,将对象头的Mark Word更新为指向锁记录的指针。如果这个更新操作成功,那么这个线程就成功获取了这个对象的轻量级锁。 如果替换成功,则该线程获取锁成功;如果失败,则表示已经有其他线程获取了锁,则该锁状态就会升级到重量级锁状态。 触发条件:当有另一个线程尝试获取已被偏向的锁时,偏向锁会升级为轻量级锁。 为什么需要将对象头中的Mark Word复制到线程栈中? 在做CAS之前,需要将对象头中的Mark Word复制到线程栈中的锁记录(Lock Record),之所以这么做的主要原因是为了保留对象的原始信息,复制Mark Word到线程栈中是为了在锁释放时能够恢复对象头的原始状态。因为锁的获取与释放是成对出现的,所以在释放锁时,JVM需要使用这份复制的原始Mark Word来恢复对象头,确保对象状态的正确性。 重量级锁(Heavyweight Locking) 当轻量级锁的CAS自旋多次失败,锁会进一步膨胀为重量级锁。 ...

March 22, 2026 · 1 min · santu

ThreadLocal为什么会导致内存泄漏?如何解决的?

典型回答 ✅什么是ThreadLocal,如何实现的? ThreadLocal的内存泄露问题是一个比较典型的问题,可以说这个问题,ThreadLocal帮我们解决了一半,还有一半需要开发者自己解决。 (装B时刻:现在是2025年9月19日,JDK25在两天前发布了,新出了一个ScopedValue,可以解决这个内存泄漏的问题,你如果面试的时候主动提一句,会让面试官觉得你很有技术热情。很加分!!!) ✅内存泄漏和内存溢出的区别是什么? 内存泄漏来自哪? 会导致ThreadLocal内存泄漏的部分其实就是他在堆上存储的ThreadLocalMap中的K-V部分: ThreadLocalMap的key就是ThreadLocal对象,他有两个引用源,一个是栈上的ThreadLocal引用,一个是ThreadLocalMap中的Key对他的引用。 而对于value来说,他的引用就一条,就是从Thread对象过来的。 所以,就会出现以下两种情况: 1、栈上的ThreadLocal Ref引用不在使用了,即方法结束后这个对象引用就不再用了,那么,ThreadLocal对象因为还有一条引用链在,所以就会导致他无法被回收,久而久之可能就会对导致OOM。 2、Thread对象如果一直在被使用,比如在线程池中被重复使用,那么从这条引用链就一直在,那么就会导致ThreadLocalMap无法被回收。 弱引用解决内存泄漏 我们先来说说JDK自己帮我们实现的一部分功能,主要是解决上面说的第一种情况: 栈上的ThreadLocal Ref引用不在使用了,即方法结束后这个对象引用就不再用了,那么,ThreadLocal对象因为还有一条引用链在,所以就会导致他无法被回收,久而久之可能就会对导致OOM。 为了解决这个问题,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; } } } ...

March 22, 2026 · 1 min · santu

为什么不建议通过Executors构建线程池

典型回答 Executors类看起来功能还是比较强大的,又用到了工厂模式、又有比较强的扩展性,重要的是用起来还比较方便,如: 1 ExecutorService executor = Executors.newFixedThreadPool(nThreads) ; 即可创建一个固定大小的线程池。 但是为什么在阿里巴巴Java开发手册中也明确指出,不允许使用Executors创建线程池呢 Executors存在什么问题 在阿里巴巴Java开发手册中提到,使用Executors创建线程池可能会导致OOM(OutOfMemory ,内存溢出),但是并没有说明为什么,那么接下来我们就来看一下到底为什么不允许使用Executors? 我们先来一个简单的例子,模拟一下使用Executors导致OOM的情况。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /** * @author Hollis */ public class ExecutorsDemo { private static ExecutorService executor = Executors.newFixedThreadPool(15); public static void main(String[] args) { for (int i = 0; i < Integer.MAX_VALUE; i++) { executor.execute(new SubThread()); } } } class SubThread implements Runnable { @Override public void run() { try { Thread.sleep(10000); } catch (InterruptedException e) { //do nothing } } } 通过指定JVM参数:-Xmx8m -Xms8m 运行以上代码,会抛出OOM: ...

March 22, 2026 · 2 min · santu

留言给博主