戴老师的博客 - 多线程-并发
type: channel
link: https://tushu.info/category/%E5%A4%9A%E7%BA%BF%E7%A8%8B-%E5%B9%B6%E5%8F%91/
description: 多线程-并发
items: Java的Lock小结
items: JAVA并发编程学习笔记之CLH队列锁
items: Java理论与实践:非阻塞算法简介
items: 您还有心跳吗？超时机制分析
items: AbstractQueuedSynchronizer源码解析之ReentrantLock
items: 5000字 | 24张图带你彻底理解Java中的21种锁
items: Java单例对象同步问题探讨
items: JDK 7 中的 Fork/Join 模式
items: JAVA并发编程学习笔记之synchronized
items: Java多线程设计模式(两阶段终止模式.html)

Java的Lock小结
type: item
date: 2026-07-09T04:39:21+00:00
description: 编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。多核时代摩尔定律告诉我们：当价格不变时，集成电路上可容纳的晶体管数目，约每隔 18 个月便会增加一倍，性能也将提升一倍。换言之，每一美元所能买到的电脑性能，将每隔 18 个月翻两倍以上。然而最近摩尔定律似乎遇到了麻烦，目前微处理器的集成度似乎到了极限，在目前的制造工艺和体系架构下很难再提高单个处理器的速度了，否则它就被烧坏了。所以现在的芯片制造商改变了策略，转而在一个电路板上集成更多的处理器，也就是我们现在常见的多核处理器。这就给软件行业带来麻烦（也可以说带来机会，比如说就业机会，呵呵）。原来的情况是：我买一台频率比原来快一倍的处理器，那么我的程序就比原来快一倍，软件工程师什么也不用干。现在不一样了，我买一台双核的处理器，我的程序和原来一样慢，当然这台机器同时处理的任务可以变多了，但是对于单个任务来说并没有帮助。在几年前，并发 (Concurrency.html) 和并行 (Parallelism.html) 程序设计还是在少量的地方使用，现在在个人的 PC 机上已经是很常见了。（Concurrency 和 Parallelism 的区别参考 这个帖子）造个诸葛亮的价钱远远高于造三个臭皮匠！多核是在一台机器上的并发，但是单机也是会到极限，所以分布式的计算也是类似的思路，用大量普通的机器协作完成一项任务。但是要想编写一个正确并且高效的能利用多核的多线程程序不是件容易的事，更别说分布式的情况（网络问题，机器故障，负载均衡，...）。现在的编译器没有办法把单线程的程序自动编译成一个多线程的版本（如果到了那一天，估计所有的程序员就失业了）。所以只能提供一些语言上的支持（比如 Scala/Erlang) 或者 MapReduce 这样的框架。Java 虽然没有提供 Scala 那样的基于消息的模型，但是也提供了丰富的 Concurrent 特性，并且屏蔽了平台的相关性（这不是件容易的事，比如多个处理器有自己的缓存，他们写的东西不会立刻被其它处理器看到），下面我们看看 Java 的内存模型 (JMM.html)。Java 内存模型 (JMM.html)并行程序有很多模型，比如共享内存模型，消息传递模型等等。这些模型或多或少的利用了平台相关的特性（在并行程序设计里很难回避平台的特性以便高效的通信），Java 抽象出了自己的内存模型，使得开发人员看不到平台的差异（这不是件容易的事），不过即使这样，和传统程序不同，我们还是不能完全不了解一些体系架构的细节问题，至少我们得了解一些。在共享内存的多处理器体系架构里（我们现在用的服务器甚至笔记本都是），每个处理器都有自己的局部缓存并定期的使之与内存同步。不同的处理器架构保证了不同程度的缓存一致性 (Cache Coherence.html)，所以操作系统，编译器和运行时环境必须一起努力来弥补平台的差异性。让每个处理器都知道其它处理器的状态的代价是非常昂贵的，所以大多数架构都不会保证一致性，这通常不会有什么问题：进程/线程直接并不共享信息，编译器可以调整代码执行顺序以便提高效率，我们都很开心。当然也有需要在线程之间进行同步的时候，比如某个线程要读取到另一个线程写入的信息，这个时候缓存里的数据就得同步到内存里才行。所以这些体系架构都提供了一些指令来完成数据的同步（当然这些指令是非常费时的，能不做就尽量不做）。这些指令一般叫做 Memory Barriers or Fences。当然只是很底层的一些东西，所幸 Java 提供了一些高层的抽象，让我们的生活变得容易一些。Sequential Consistency：我们假设一个线程执行（可能在多个处理器上切换），每个变量读取到的值都是最新的修改（也就是 Cache 里的立马生效），这样得到的结果是我们预期的。但是让我们意外的事情是：如果我们不做任何事情，那么很可能会出现错误，比如下面的这个例子：public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while (!ready.html) Thread.yield(); System.out.println(number.html); } } public static void main(String[] args.html) { new ReaderThread().start(); number = 42; ready = true;…

JAVA并发编程学习笔记之CLH队列锁
type: item
date: 2026-07-09T04:39:15+00:00
description: NUMA 与 SMPSMP（Symmetric Multi-Processor，对称多处理器）结构指服务器中多个 CPU 对称工作，每个 CPU 访问内存地址所需时间相同。其主要特征是共享，包含对 CPU、内存、I/O 等资源进行共享。SMP 的优点是能够保证内存一致性；缺点是这些共享资源很可能成为性能瓶颈。随着 CPU 数量的增加，每个 CPU 都要访问相同的内存资源，可能导致内存访问冲突，进而造成 CPU 资源的浪费。常用的 PC 机就属于这种架构。NUMA（Non-Uniform Memory Access，非一致存储访问）将 CPU 分为多个 CPU 模块，每个模块由多个 CPU 组成，并且具有独立的本地内存、I/O 槽口等。模块之间可以通过互联模块相互访问，访问本地内存的速度将远远高于访问远地内存（系统内其它节点的内存）的速度，这也是“非一致存储访问”名称的由来。NUMA 的优点是可以较好地解决原来 SMP 系统的扩展问题；缺点是由于访问远地内存的延时远远超过本地内存，因此当 CPU 数量增加时，系统性能无法线性增加。CLH 算法实现CLH 队列中的结点 QNode 中含有一个 locked 字段：该字段若为 true，表示该线程需要获取锁且未释放锁；若为 false，表示线程已释放锁。结点之间通过隐式的链表相连。之所以称为“隐式链表”，是因为这些结点之间没有明显的 next 指针，而是通过 myPred 所指向结点的变化情况来影响 myNode 的行为。CLHLock 上还有一个尾指针 tail，始终指向队列的最后一个结点。CLHLock 的类图如下所示：当一个线程需要获取锁时，会创建一个新的 QNode，将其中的 locked 设置为 true 表示需要获取锁。然后线程对 tail 域调用 getAndSet 方法，使自己成为队列的尾部，同时获取一个指向其前驱的引用 myPred。随后，该线程就在前驱结点的 locked 字段上进行自旋（Spin），直到前驱结点释放锁。当一个线程需要释放锁时，将当前结点的 locked 域设置为 false，同时回收前驱结点。如下图所示，线程 A 需要获取锁，其 myNode 域为 true，此时 tail 指向线程 A 的结点；然后线程 B 也加入到线程 A 后面，tail 指向线程 B 的结点。接着线程 A 和 B 都在它的 myPred 域上自旋，一旦它的 myPred 结点的 locked 字段变为 false，它就可以获取锁并执行临界区代码。显然，线程 A 的 myPred locked 域为 false，此时线程 A 获取到了锁。整个 CLH 的代码如下。其中用到了 ThreadLocal 类，将 QNode 绑定到每一个线程上；同时用到了 AtomicReference，对尾指针的修改正是调用它的 getAndSet() 操作来实现的，它能够保证以原子方式更新对象引用。public class CLHLock implements Lock { AtomicReference<QNode> tail = new AtomicReference<QNode>(new QNode(.html)); ThreadLocal<QNode> myPred; ThreadLocal<QNode> myNode; public CLHLock() { tail = new AtomicReference<QNode>(new QNode(.html)); myNode = new ThreadLocal<QNode>() { protected QNode initialValue() { return new QNode(); } }; myPred = new ThreadLocal<QNode>() { protected QNode initialValue() { return null; } }; } @Override public void lock() { QNode qnode = myNode.get(); qnode.locked = true; QNode pred = tail.getAndSet(qnode.html); myPred.set(pred.html); while (pred.locked) { // 自旋等待 } } @Override public void unlock() { QNode qnode = myNode.get(); qnode.locked = false; myNode.set(myPred.get(.html)); } }从代码中可以看出，lock 方法中有一个 while 循环，这是在等待前驱结点的 loc…

Java理论与实践:非阻塞算法简介
type: item
date: 2026-07-09T04:38:31+00:00
description: *在不只一个线程访问一个互斥的变量时，所有线程都必须使用同步，否则就可能会发生一些非常糟糕的事情。Java 语言中主要的同步手段就是 synchronized 关键字（也称为 内在锁 ），它强制实行互斥，确保执行 synchronized 块的线程的动作，能够被后来执行受相同锁保护的 synchronized 块的其他线程看到。在使用得当的时候，内在锁可以让程序做到线程安全，但是在使用锁定保护短的代码路径，而且线程频繁地争用锁的时候，锁定可能成为相当繁重的操作。在 “流行的原子” 一文中，我们研究了 原子变量 ，原子变量提供了原子性的读-写-修改操作，可以在不使用锁的情况下安全地更新共享变量。原子变量的内存语义与 volatile 变量类似，但是因为它们也可以被原子性地修改，所以可以把它们用作不使用锁的并发算法的基础。非阻塞的计数器清单 1 中的 Counter 是线程安全的，但是使用锁的需求带来的性能成本困扰了一些开发人员。但是锁是必需的，因为虽然增加看起来是单一操作，但实际是三个独立操作的简化：检索值，给值加 1，再写回值。（在 getValue 方法上也需要同步，以保证调用 getValue 的线程看到的是最新的值。虽然许多开发人员勉强地使自己相信忽略锁定需求是可以接受的，但忽略锁定需求并不是好策略。）在多个线程同时请求同一个锁时，会有一个线程获胜并得到锁，而其他线程被阻塞。JVM 实现阻塞的方式通常是挂起阻塞的线程，过一会儿再重新调度它。由此造成的上下文切换相对于锁保护的少数几条指令来说，会造成相当大的延迟。清单 1. 使用同步的线程安全的计数器public final class Counter { private long value = 0; public synchronized long getValue() { return value; } public synchronized long increment() { return ++value; } } 显示更多Show more icon清单 2 中的 NonblockingCounter 显示了一种最简单的非阻塞算法：使用 AtomicInteger 的 compareAndSet() （CAS）方法的计数器。 compareAndSet() 方法规定 “将这个变量更新为新值，但是如果从我上次看到这个变量之后其他线程修改了它的值，那么更新就失败”（请参阅 “流行的原子” 获得关于原子变量以及 “比较和设置” 的更多解释。）清单 2. 使用 CAS 的非阻塞算法public class NonblockingCounter { private AtomicInteger value; public int getValue() { return value.get(); } public int increment() { int v; do { v = value.get(); while (!value.compareAndSet(v, v + 1.html)); return v + 1; } } 显示更多Show more icon原子变量类之所以被称为 原子的 ，是因为它们提供了对数字和对象引用的细粒度的原子更新，但是在作为非阻塞算法的基本构造块的意义上，它们也是原子的。非阻塞算法作为科研的主题，已经有 20 多年了，但是直到 Java 5.0 出现，在 Java 语言中才成为可能。现代的处理器提供了特殊的指令，可以自动更新共享数据，而且能够检测到其他线程的干扰，而 compareAndSet() 就用这些代替了锁定。（如果要做的只是递增计数器，那么 AtomicInteger 提供了进行递增的方法，但是这些方法基于 compareAndSet() ，例如 NonblockingCounter.increment() ）。非阻塞版本相对于基于锁的版本有几个性能优势。首先，它用硬件的原生形态代替 JVM 的锁定代码路径，从而在更细的粒度层次上（独立的内存位置）进行同步，失败的线程也可以立即重试，而不会被挂起后重新调度。更细的粒度降低了争用的机会，不用重新调度就能重试的能力也降低了争用的成本。即使有少量失败的 CAS 操作，这种方法仍然会比由于锁争用造成的重新调度快得多。NonblockingCounter 这个示例可能简单了些，但是它演示了所有非阻塞算法的一个基本特征 —— 有些算法步骤的执行是要冒险的，因为知道如果 CAS 不成功可能不得不重做。非阻塞算法通常叫作 乐观算法 ，因为它们继续操作的假设是不会有干扰。如果发现干扰，就会回退并重试。在计数器的示例中，冒险的步骤是递增 —— 它检索旧值并在旧值上加一，希望在计算更新期间值不会变化。如…

您还有心跳吗？超时机制分析
type: item
date: 2026-07-09T04:37:53+00:00
description: 问题描述在 C/S（Client/Server）架构中，为了减少频繁建立连接的开销，我们有时会长时间保持一个连接。但同时，通常会设置一个超时时间：若在该时间内连接未发起任何请求，则将其断开，以减少服务端负载并节约资源。该机制一般建议在服务端实现。这是因为当客户端强制关闭或意外断开连接时，服务端往往无法立即感知。若将超时机制完全交由客户端实现，在上述异常情况下，该机制可能会失效。这一问题看似普通，往往容易被忽视。但最近在项目中观察到一种该机制的糟糕实现，故在此深入分析其潜在问题及优化方案。问题分析及解决方案服务端通常需要维护大量连接，因此普遍做法是创建一个定时器，定期检查所有连接中哪些已超时。此外，关键在于：当收到客户端发来的数据时，如何高效地刷新该连接的超时信息？初始实现与可见性问题最近看到的一种实现方式如下：public class Connection { private long lastTime; public void refresh() { lastTime = System.currentTimeMillis(); } public long getLastTime() { return lastTime; } // ...... }在每次收到客户端数据时，调用 refresh 方法更新最后活动时间。然后在定时器任务中，通过比较当前时间与每个连接的 getLastTime() 来判定是否超时：public class TimeoutTask extends TimerTask { public void run() { long now = System.currentTimeMillis(); for (Connection c : connections.html) { if (now - c.getLastTime(.html) > TIMEOUT_THRESHOLD) { // timeout, do something } } } }熟悉并发编程的读者可能已经发现了问题：内存可见性。调用 refresh 方法的线程（通常是 IO 线程）与执行定时器的线程并非同一个线程。若 lastTime 未做同步处理，定时器线程读到的可能是旧值，导致将活跃连接误判为超时并断开。性能考量与测试有读者可能会想到使用 volatile 修饰 lastTime。这确实解决了可见性问题，但作为服务端，高性能往往是核心诉求。volatile 写操作以及频繁调用 System.currentTimeMillis() 是否存在性能瓶颈？以下是我本地环境的一组测试数据，代码供参考：public class PerformanceTest { private static long i; private volatile static long vt; private static final int TEST_SIZE = 10000000; public static void main(String[] args.html) { long time = System.nanoTime(); for (int n = 0; n < TEST_SIZE; n++.html) vt = System.currentTimeMillis(); System.out.println("volatile 写 + 取系统时间：" + (-time + (time = System.nanoTime(.html)))); for (int n = 0; n < TEST_SIZE; n++.html) i = System.currentTimeMillis(); System.out.println("普通写 + 取系统时间：" + (-time + (time = System.nanoTime(.html)))); for (int n = 0; n < TEST_SIZE; n++.html) synchronized (PerformanceTest.class) { } System.out.println("空的同步块（synchronized）：" + (-time + (time = System.nanoTime(.html)))); for (int n = 0; n < TEST_SIZE; n++.html) vt++; System.out.println("volatile 变量自增：" + (-time + (time = System.nanoTime(.html)))); for (int n = 0; n < TEST_SIZE; n++.html) vt = i; System.out.println("v…

AbstractQueuedSynchronizer源码解析之ReentrantLock
type: item
date: 2026-07-09T04:37:09+00:00
description: 前言在 JDK 1.8 中，AbstractQueuedSynchronizer（简称 AQS）拥有众多子类，如下图所示：可以看到，公平锁与非公平锁（包括 ReentrantLock）都是基于 AQS 实现的。公平锁（Fair Lock）：线程抢占锁的顺序严格按照调用 lock 方法的先后顺序，依次获取锁。非公平锁（Unfair Lock）：线程抢占锁的顺序不定。线程尝试获取锁时，无论是否有其他线程在等待，都先尝试直接获取。若失败则加入等待队列。这意味着后来者可能“插队”成功（但因为抢占锁失败而加入到等待队列的线程不能参与下一次抢占，直到被 unpark）。AQS 核心结构AQS 通过内置的 FIFO 同步队列来完成资源获取线程的排队工作。如果当前线程获取同步状态失败（即获取锁失败），AQS 会将当前线程及等待状态等信息构造成一个节点（Node）并加入同步队列，同时阻塞当前线程。当同步状态释放时，则会把节点中的线程唤醒，使其再次尝试获取同步状态。类结构public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { protected AbstractQueuedSynchronizer() { } // 同步器队列头结点 private transient volatile Node head; // 同步器队列尾结点 private transient volatile Node tail; // 同步状态（当 state 为 0 时，无锁；当 state > 0 时，说明有锁） private volatile int state; // 获取锁状态 protected final int getState() { return state; } // 设置锁状态 protected final void setState(int newState.html) { state = newState; } // ... 其他方法 }通过 AQS 的类结构可以看到，其内部核心包含一个队列和一个 state 变量：队列：通过双向链表实现，用于存储等待获取锁的线程。state：锁的状态标志。volatile 关键字：head、tail 和 state 都是 volatile 类型的变量，保证了多线程环境下的内存可见性。同步队列的基本结构如下：Node 节点结构static final class Node { static final Node SHARED = new Node(); static final Node EXCLUSIVE = null; // 表示当前的线程被取消 static final int CANCELLED = 1; // 表示当前节点的后继节点包含的线程需要运行，也就是 unpark static final int SIGNAL = -1; // 表示当前节点在等待 condition，也就是在 condition 队列中 static final int CONDITION = -2; // 表示当前场景下后续的 acquireShared 能够得以执行 static final int PROPAGATE = -3; // 表示节点的状态。默认为 0，表示当前节点在 sync 队列中，等待着获取锁 volatile int waitStatus; // 前驱节点 volatile Node prev; // 后继节点 volatile Node next; // 获取锁的线程 volatile Thread thread; // 存储 condition 队列中的后继节点 Node nextWaiter; }Node 节点中除了存储当前线程、节点类型、队列中前后元素的引用外，还有一个关键变量 waitStatus。该变量用于描述节点的状态。由于 AQS 队列中存在并发，队列中会存取一定数量的节点，每个节点代表了一个线程的状态。有的线程可能因超时或中断需要放弃竞争（取消），有的线程在等待条件满足（Condition）。因此需要一个变量来描述这些状态，waitStatus 主要有以下几种状态：static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3;CANCELLED (1.html)：因为超时或者中断，节点会被设置为取消状态。被取消…

5000字 | 24张图带你彻底理解Java中的21种锁
type: item
date: 2026-07-09T04:36:07+00:00
description: 本篇主要内容如下：帮你总结好的锁：序号锁名称应用1乐观锁CAS2悲观锁synchronized、Vector、Hashtable3自旋锁CAS4可重入锁synchronized、ReentrantLock、Lock5读写锁ReentrantReadWriteLock、CopyOnWriteArrayList、CopyOnWriteArraySet6公平锁ReentrantLock(true.html)7非公平锁synchronized、ReentrantLock(false.html)8共享锁ReentrantReadWriteLock 中读锁9独占锁synchronized、Vector、Hashtable、ReentrantReadWriteLock 中写锁10重量级锁synchronized11轻量级锁锁优化技术12偏向锁锁优化技术13分段锁ConcurrentHashMap14互斥锁synchronized15同步锁synchronized16死锁相互请求对方的资源17锁粗化锁优化技术18锁消除锁优化技术1. 乐观锁乐观锁（Optimistic Locking） 是一种基于乐观思想的并发控制机制。它假定当前环境是“读多写少”，发生并发冲突的概率较低。因此，在读数据时认为其他线程不会进行修改，所以不上锁。只有在写数据时，才会判断当前值与期望值是否相同：如果相同则进行更新（更新期间保证原子性），否则继续尝试 CAS 操作。Java 中的实现： 主要通过 CAS（Compare-And-Swap，比较并交换）实现。比较主内存中的当前值与线程工作内存中的预期值，若一致则更新，否则重试。如上图所示，乐观锁允许同时进行读操作，但在写操作时会检查冲突。2. 悲观锁悲观锁（Pessimistic Locking） 是一种基于悲观思想的并发控制机制。它认为写多读少，发生并发冲突的可能性高。因此，每次去拿数据的时候都认为其他线程会修改，所以每次读写数据时都会上锁。其他线程想要读写这个数据时，会被阻塞（Block），直到持有锁的线程释放锁。Java 中的实现： synchronized 修饰的方法和方法块、ReentrantLock 等。如上图所示，同一时刻只能有一个线程进行读操作或者写操作，其他线程的读写操作均被阻塞。3. 自旋锁自旋锁（Spin Lock） 是一种技术策略。为了让线程等待，不让线程立即进入阻塞状态，而是让线程执行一个忙循环（自旋），反复检查锁是否可用。现代个人电脑和服务器多为多核处理器系统。如果物理机器有一个以上的处理器核心，能让两个或以上的线程同时并行执行，就可以让后面请求锁的那个线程“稍等一会”，但不放弃处理器的执行时间，看看持有锁的线程是否很快就会释放锁。自旋锁 的优点： 避免了线程切换的开销。挂起线程和恢复线程的操作都需要转入内核态中完成，这些操作给 Java 虚拟机的并发性能带来了很大的压力。自旋锁 的缺点： 占用处理器的时间。如果占用的时间很长，会白白消耗处理器资源，而不会做任何有价值的工作，带来性能的浪费。因此自旋等待的时间必须有一定的限度，如果自旋超过了限定的次数仍然没有成功获得锁，就应当使用传统的方式去挂起线程。自旋次数默认值： 10 次，可以使用参数 -XX:PreBlockSpin 来自行更改（注：该参数在较新版本 JDK 中可能已废弃或调整）。自适应自旋： 意味着自旋的时间不再是固定的，而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定的。有了自适应自旋，随着程序运行时间的增长及性能监控信息的不断完善，虚拟机对程序锁的状态预测就会越来越精准。Java 中的实现： CAS 操作中的比较操作失败后的自旋等待。4. 可重入锁（递归锁）可重入锁（Reentrant Lock） 是一种技术特性。任意线程在获取到锁之后能够再次获取该锁而不会被锁所阻塞。可重入锁 的原理： 通过组合自定义同步器来实现锁的获取与释放。再次获取锁： 识别获取锁的线程是否为当前占据锁的线程，如果是，则再次成功获取。获取锁后，进行计数自增。释放锁： 释放锁时，进行计数自减。Java 中的实现： ReentrantLock、synchronized 修饰的方法或代码段。可重入锁 的作用： 避免死锁，支持递归调用。面试题 1： 可重入锁如果加了两把，但是只释放了一把会出现什么问题？答：程序卡死，线程不能出来。也就是说我们申请了几把锁，就需要释放几把锁。面试题 2： 如果只加了一把锁，释放两次会出现什么问题？答：会报错，java.lang.IllegalMonitorStateException。5. 读写锁读写锁（ReadWrite Lock） 是一种技术机制，通过 ReentrantReadWriteLock 类来实现。为了提高性…

Java单例对象同步问题探讨
type: item
date: 2026-07-09T04:35:38+00:00
description: Java 单例对象同步问题探讨单例对象（Singleton）是一种常用的设计模式。在 Java 应用中，单例对象能保证在一个 JVM 中，该对象只有一个实例存在。正是由于这个特点，单例对象通常作为程序中存放配置信息的载体，因为它能保证其他对象读到一致的信息。例如在某个服务器程序中，服务器的配置信息可能存放在数据库或文件中，这些配置数据由某个单例对象统一读取。服务进程中的其他对象如果要获取这些配置信息，只需访问该单例对象即可。这种方式极大地简化了复杂环境下（尤其是多线程环境）的配置管理，但随着应用场景的不同，也可能带来一些同步问题。本文将探讨在多线程环境下，使用单例对象作配置信息管理时可能会带来的几个同步问题，并针对每个问题给出可选的解决办法。问题描述在多线程环境下，单例对象的同步问题主要体现在两个方面：单例对象的初始化和单例对象的属性更新。本文描述的方法基于如下假设：单例对象的属性（或成员变量）的获取，是通过单例对象的初始化实现的。即在单例对象初始化时，会从文件或数据库中读取最新的配置信息。其他对象不能直接改变单例对象的属性，单例对象属性的变化来源于配置文件或配置数据库数据的变化。1.1 单例对象的初始化首先讨论单例对象的初始化同步。单例模式的通常处理方式是：在对象中有一个静态成员变量，其类型就是单例类型本身；如果该变量为 null，则创建该单例类型的对象，并将该变量指向这个对象；如果该变量不为 null，则直接使用该变量。其过程如下面代码所示：public class GlobalConfig { private static GlobalConfig instance = null; private Vector properties = null; private GlobalConfig() { // Load configuration information from DB or file // Set values for properties } public static GlobalConfig getInstance() { if (instance == null.html) { instance = new GlobalConfig(); } return instance; } public Vector getProperties() { return properties; } }这种处理方式在单线程模式下可以很好地运行，但是在多线程模式下可能产生问题。如果第一个线程发现成员变量为 null 准备创建对象，此时第二个线程同时也发现成员变量为 null，也会创建新对象。这就会造成在一个 JVM 中有多个单例类型的实例。如果这个单例类型的成员变量在运行过程中变化，会造成多个单例类型实例的不一致，产生一些异常现象。例如，某服务进程通过检查单例对象的某个属性来停止多个线程服务，如果存在多个单例对象的实例，就会造成部分线程服务停止，部分线程服务不能停止的情况。1.2 单例对象的属性更新通常，为了实现配置信息的实时更新，会有一个线程持续检测配置文件或配置数据库的内容，一旦发现变化，就更新到单例对象的属性中。在更新这些信息的时候，很可能还会有其他线程正在读取这些信息，造成不可预知的后果。还是以通过单例对象属性停止线程服务为例，如果更新属性时读写不同步，可能访问该属性时这个属性正好为空（null），程序就会抛出异常。解决方案2.1 单例对象的初始化同步对于初始化的同步，可以通过如下代码所采用的方式解决：public class GlobalConfig { private static GlobalConfig instance = null; private Vector properties = null; private GlobalConfig() { // Load configuration information from DB or file // Set values for properties } private static synchronized void syncInit() { if (instance == null.html) { instance = new GlobalConfig(); } } public static GlobalConfig getInstance() { if (instance == null.html) { syncInit(); } return instance; } public Vector getProperties() { return properties; } }这种处理方式虽然引入了同步代码，但是因为这段同步代码只会在最开始…

JDK 7 中的 Fork/Join 模式
type: item
date: 2026-07-09T04:35:08+00:00
description: JDK 7 中的 Fork/Join 模式介绍随着多核芯片逐渐成为主流，大多数软件开发人员不可避免地需要了解并行编程的知识。与此同时，主流编程语言正在将越来越多的并行特性合并到标准库或语言本身之中。Java 开发工具包（JDK）在这方面同样走在潮流的前方。在 JDK 5 中，由 Doug Lea 提供的并行框架成为了标准库的一部分（JSR-166）。随后，在 JDK 6 中，一些新的并行特性，例如并行 Collection 框架，被合并到了标准库中（JSR-166x）。在本文撰写时，尽管 Java SE 7 尚未正式发布，但一些并行相关的新特性已经出现在 JSR-166y 中：Fork/Join 模式；TransferQueue：继承自 BlockingQueue，能在队列满时阻塞“生产者”；ArrayTasks/ListTasks：用于并行执行某些数组/列表相关任务的类；IntTasks/LongTasks/DoubleTasks：用于并行处理数字类型数组的工具类，提供了排序、查找、求和、求最小值、求最大值等功能。其中，对 Fork/Join 模式 的支持可能是对开发并行软件来说最通用的新特性。在 JSR-166y 中，Doug Lea 实现 ArrayTasks、ListTasks 等类时就大量用到了 Fork/Join 模式。读者需要注意，因为本文基于 JDK 7 发布前的 JSR-166y 版本，涉及到的功能和最终发布版本可能存在差异。Fork/Join 模式有自己的适用范围。如果一个应用能被分解成多个子任务，并且组合多个子任务的结果就能够获得最终的答案，那么这个应用就适合用 Fork/Join 模式来解决。图 1 给出了一个 Fork/Join 模式的示意图，位于图上部的 Task 依赖于位于其下的 Task 的执行，只有当所有的子任务都完成之后，调用者才能获得 Task 0 的返回结果。图 1. Fork/Join 模式示意图可以说，Fork/Join 模式能够解决很多种类的并行问题。通过使用 Doug Lea 提供的 Fork/Join 框架，软件开发人员只需要关注任务的划分和中间结果的组合，就能充分利用并行平台的优良性能。其他和并行相关的诸多难于处理的问题，例如负载平衡、同步等，都可以由框架采用统一的方式解决。这样，我们就能够轻松地获得并行的好处，而避免了并行编程困难且容易出错的缺点。使用 Fork/Join 模式在开始尝试 Fork/Join 模式之前，我们需要从 Doug Lea 主持的 Concurrency JSR-166 Interest Site 上下载 JSR-166y 的源代码，并且需要安装最新版本的 JDK 6（下载网址请参阅 参考资源）。Fork/Join 模式的使用方式非常直观。首先，我们需要编写一个 ForkJoinTask 来完成子任务的分割、中间结果的合并等工作。随后，我们将这个 ForkJoinTask 交给 ForkJoinPool 来完成应用的执行。通常我们并不直接继承 ForkJoinTask，因为它包含了太多的抽象方法。针对特定的问题，我们可以选择 ForkJoinTask 的不同子类来完成任务。RecursiveAction 是 ForkJoinTask 的一个子类，它代表了一类最简单的 ForkJoinTask：不需要返回值，当子任务都执行完毕之后，不需要进行中间结果的组合。如果我们从 RecursiveAction 开始继承，那么我们只需要重载 protected void compute() 方法。下面，我们来看看怎么为快速排序算法建立一个 ForkJoinTask 的子类：清单 1. ForkJoinTask 的子类class SortTask extends RecursiveAction { final long[] array; final int lo; final int hi; private int THRESHOLD = 30; public SortTask(long[] array.html) { this.array = array; this.lo = 0; this.hi = array.length - 1; } public SortTask(long[] array, int lo, int hi.html) { this.array = array; this.lo = lo; this.hi = hi; } protected void compute() { if (hi - lo < THRESHOLD.html) sequentiallySort(array, lo, hi.html); else { int pivot …

JAVA并发编程学习笔记之synchronized
type: item
date: 2026-07-09T04:35:01+00:00
description: 监视器Java 中的同步是通过监视器（Monitor）模型来实现的。Java 中的监视器实质是一个代码块，这段代码块在同一时刻只允许被一个线程执行。线程要想执行这段代码块，唯一的方式是获得监视器。监视器支持两种同步方式：互斥与协作。互斥：多线程环境下，如果线程之间需要共享数据，必须解决互斥访问的问题。监视器可以确保监视器上的数据在同一时刻只会有一个线程在访问。协作：某些场景需要线程间协作。例如，一个线程向缓冲区写数据，另一个线程从缓冲区读数据。如果读线程发现缓冲区为空就会等待，当写线程向缓冲区写入数据后，会唤醒读线程。这里读线程和写线程就是一种合作关系。JVM 通过 Object 类的 wait 方法来使线程等待。在调用 wait 方法后，该线程会释放它持有的监视器，直到其他线程通知它才有执行的机会。一个线程调用 notify 方法通知正在等待的线程，但被唤醒的线程并不会马上执行，而是要等到通知线程释放监视器后，它重新获取监视器才有执行的机会。如果刚好唤醒的这个线程需要的监视器被其他线程抢占，那么这个线程会继续等待。Object 类中的 notifyAll 方法可以解决这个问题，它可以唤醒所有等待的线程，总有一个线程能获得执行机会。 如上图所示，一个线程通过 1 号门进入 Entry Set（入口区）。如果在入口区没有线程等待，那么这个线程就会获取监视器成为监视器的 Owner，然后执行监视区域的代码。如果在入口区中有其它线程在等待，那么新来的线程也会和这些线程一起等待。线程在持有监视器的过程中，有两个选择：正常执行监视器区域的代码，释放监视器，通过 5 号门退出监视器。等待某个条件的出现，于是它会通过 3 号门到 Wait Set（等待区） 休息，直到相应的条件满足后再通过 4 号门进入，重新获取监视器再执行。注意：当一个线程释放监视器时，在入口区和等待区的等待线程都会去竞争监视器。如果入口区的线程赢了，会从 2 号门进入；如果等待区的线程赢了，会从 4 号门进入。只有通过 3 号门才能进入等待区，在等待区中的线程只有通过 4 门才能退出等待区。也就是说，一个线程只有在持有监视器时才能执行 wait 操作，处于等待的线程只有再次获得监视器才能退出等待状态。对象锁JVM 中的一些数据，比如堆和方法区，会被所有线程共享。Java 中每个对象和类实际上都有一把锁与之相关联：对于对象来说，监视的是这个对象的实例变量。对于类来说，监视的是类变量。如果一个对象没有实例变量，就什么也不监视。当虚拟机装载类时，会创建一个 Class 类的实例，锁住一个类实际上锁住的是这个类对应的 Class 类的实例。对象锁是可重入的，也就是说对一个对象或者类上的锁可以累加。在 Java 中有两种监视区域：同步方法和同步块。这两种监视区域都和一个引用对象相关联。当到达这个监视区域时，JVM 就会锁住这个引用对象；不论它是怎么离开的，都会释放这个引用对象上的锁。Java 程序员不能自己手动加对象锁，对象锁是 JVM 内部机制，只需要编写同步方法或者同步块即可，操作监视区域时 JVM 会自动帮你上锁或者释放锁。同步语句要建立一个同步语句，只需要在相关语句加上 synchronized 关键字就可以。例如下面的 incr 方法，如果没有获得当前对象（this）的锁，在同步块内的语句是不会执行的。如果不是 this 引用，而是用另一个对象的引用，需要获得对应对象的锁同步块才会执行；如果用表达式获得对 Class 对象实例的引用，就需要锁住那个类。void incr() { synchronized (this.html) { i++; } }以下是 incr 方法生成的字节码序列： void incr(); Code: 0: aload_0 // 将 this 引用压栈 1: dup // 复制栈顶元素 2: astore_1 // 出栈并将 this 引用存放在局部变量 1 中 3: monitorenter // 出栈并获取对象锁 4: aload_0 // 将 this 引用压栈 5: dup // 复制栈顶元素 6: getfield #17 // 获取 i 的值 9: iconst_1 // 常数 1 入栈 10: iadd // 将 i+1 的结果入栈 11: putfield #17 // 将 i 的值存入 this 中 14: aload_1 // 将 this 引用压栈 15: monitorexit // 弹出 this 引用释放对象锁 16: goto 22 // 返回 19: aload_1 // 19-22 如果抛出，释放对象锁 20: monitorexit 21: athrow 22: return Exception table: …

Java多线程设计模式(两阶段终止模式.html)
type: item
date: 2026-07-09T04:34:48+00:00
description: 两阶段终止模式 (Two-Phase Termination Pattern.html)两阶段终止模式 (Two-Phase Termination Pattern.html) 指的是当希望结束一个线程时，发出终止请求后线程不会立即停止，而是先完成当前的刷新或清理工作。线程进入“终止处理中”状态，在该状态下不再执行日常工作任务，而是专注于执行终止相关的操作。设计考量采用该模式主要基于以下三个关键因素：安全结束线程必须确保线程能够安全地结束。直接使用 Thread.stop() 方法存在风险，因为它会强制停止线程，无论线程执行到哪一行代码，都无法保证资源释放或数据一致性，可能导致对象处于不一致状态。正常的终止处理必须确保能够进行正常的收尾工作。在 Java 中，这一点通常可以使用 finally 代码块来实现，确保无论何种情况下退出循环，清理逻辑都能被执行。高响应性的终止收到终止请求后，如果线程正处于 wait、sleep 或 join 等阻塞状态，不应等到超时才终止，而应立即中断这些状态，进而执行终止操作。工作流程当一个线程正在执行周期性工作时，若在“作业阶段”收到了停止请求，该线程不应立即离开，而应先完成本次周期内部的工作，然后进入“善后阶段”完成清理工作。所谓的两阶段终止，即中止“运作阶段”，并完成“善后阶段”，从而完整地结束线程的工作生命周期。代码实现两阶段终止线程的架构模式如下：public class WorkerTerminalThread extends Thread { // 已经送出终止请求为 true，初始化的时候为 false // 由于该字段可能会被多个线程访问修改，为了保护就使用 volatile private volatile boolean shutdownRequested = false; // 终止请求 public void shutdownRequest() { shutdownRequested = true; interrupt(); } public boolean isShutdownRequest() { return shutdownRequested; } // 具体动作 public final void run() { try { while (!shutdownRequested.html) { doWork(); } } catch (InterruptedException e.html) { // 捕获中断异常，配合标志位退出循环 } finally { // 终止处理中的工作，不会进行平常操作，但是会进行终止处理 doShutdown(); } } // 具体工作操作 private void doWork() throws InterruptedException { // 模拟周期性工作 } // 终止后进行善后处理 private void doShutdown() { // 清理资源 } }原理解析Volatile 的作用利用 volatile 修饰 shutdownRequested 字段，是因为该字段可能会被多个线程使用及修改。为了保证该字段的可见性，可以使用同步方法、同步代码块或 volatile 关键字。使用 volatile 修饰的字段，强制该成员变量在每次被线程访问时，都从共享内存中重读该成员变量的值；而且当成员变量发生变化时，强迫线程将变化值回写到共享内存。这样在任何时刻，两个不同的线程总是看到某个成员变量的同一个值。标识位与中断状态的结合这里运用了“标识位”和“中断状态”组合的方式来终止线程，之所以不单独使用其中一种，原因如下：仅利用标识位：无法使那些处于 wait、sleep 或者 join 中的线程马上停止，响应性会很差。仅利用中断 (interrupt.html)：interrupt 仅仅对于 wait、sleep 或 join 处抛出异常。如果工作代码执行在 catch 里捕获了 InterruptedException 后未做处理，则此时中断标志可能被清除，导致中断不起作用。组合使用：加入 interrupt 后可以立刻使得处于阻塞状态的线程中断并抛出异常；配合 shutdownRequested 标识位，确保即使在异常被捕获的情况下，也能通过检查标志位正常退出循环。说明：本文示例基于经典 Java 多线程模型编写。在现代 Java 并发编程中，更推荐使用 ExecutorService 配合 Runnable 或 Callable 任务，并通过 shutdown() 或 Future.cancel() 来管理线程生命周期，而非直接继承 Thread 类。但两阶段终止的核心思想（请求 + 清理）依然适用。