戴老师的博客 - java并发教程
type: channel
link: https://www.tushu.info/tag/java%E5%B9%B6%E5%8F%91%E6%95%99%E7%A8%8B/
items: Difference between lock and monitor – Java Concurrency
items: Java并发性– yield（）和join（）之间的区别
items: ConcurrentHashMap之实现细节
items: Java 理论与实践: 并发集合类
items: Java中sleep（）和wait（）之间的区别
items: Java比较和交换示例– CAS算法
items: Java并发性–线程安全性？
items: HashMap vs ConcurrentHashMap — 示例及Iterator探秘
items: Java Multi-threading Evolution and Topics
items: Java Callable Future Example

Difference between lock and monitor – Java Concurrency
type: item
date: 2026-07-09T04:39:32+00:00
description: 编者注：本文为历史博文归档；涉及 JDK、框架与工具链版本请以当前官方文档为准。引用外链图片可能失效，阅读时请注意时效性。在面试中，您可能会遇到这样的问题：锁（Lock）和监视器（Monitor）之间有什么区别？ 要回答这个问题，必须对 Java 多线程在后台的工作机制有足够的了解。答案其实很简短：锁为实现监视器提供了必要的支持。 详细的解释如下。锁（Lock）锁是一种数据结构，逻辑上是堆内存中对象标头（Object Header）的一部分。JVM 中的每个对象都具有此锁（或称互斥锁，Mutex），任何程序均可使用该锁来协调对对象的多线程访问。所有权机制：如果有任何线程想要访问该对象的实例变量，线程必须首先“拥有”对象的锁（即在锁存储区中设置特定标志）。阻塞机制：试图访问该对象变量的所有其他线程必须等待，直到拥有锁的线程释放该对象的锁（取消设置标志）。可重入性：线程拥有锁后，它可以多次请求相同的锁。但是在将锁提供给其他线程之前，必须释放相同的次数。例如，如果一个线程请求了三次锁，则该线程将继续拥有该锁，直到它“释放”了三次。请注意，当线程明确要求锁时，它是由线程获得的。在 Java 中，这是通过 synchronized 关键字或通过 wait() 和 notify() 方法来完成的。监视器（Monitor）监视器是一种同步构造，它允许线程具有互斥（使用锁）和协作功能。即使线程能够等待某些条件成立（使用等待集，Wait-Set），监视器也能管理这些行为。换句话说，连同实现锁的数据，每个 Java 对象在逻辑上均与实现等待集的数据相关联。锁的作用：帮助线程在共享数据上独立工作而不会互相干扰。等待集的作用：帮助线程相互协作，共同朝着一个目标努力。例如，所有等待线程都将移至该等待集，并在锁定被释放后收到通知。此等待集通过锁（互斥量）的辅助来帮助构建监视器。互斥（Mutual Exclusion）用简单的话来说，监视器就像一栋建筑物，其中包含一个特殊的房间（对象实例），一次只能占用一个线程。房间中通常包含一些需要防止并发访问的数据。进入监视器：从线程进入该房间的时间到它离开的时间，它都可以独占访问该房间中的任何数据。获取监视器：进入建筑物内的特殊房间。拥有监视器：占领房间。释放监视器：离开房间。退出监视器：离开整个建筑物。当线程到达以访问受保护的数据（进入特殊房间）时，首先将其放入建筑物接收队列中（入口集，Entry Set）。如果没有其他线程在等待（监视器未被拥有），则该线程获取锁并继续执行受保护的代码。线程完成执行后，它将释放锁并退出建筑物（退出监视器）。如果一个线程到达并且另一个线程已经拥有监视器，则它必须在接收队列中等待（入口集）。当前所有者退出监视器时，新到达的线程必须与也在入口集中等待的任何其他线程竞争。只有一个线程会赢得比赛并拥有锁。注意：在单纯的互斥场景中，等待集（Wait Set）并不发挥作用。协作（Cooperation）通常，互斥仅在多个线程共享数据或其他资源时才重要。如果两个线程不使用任何公共数据或资源，则它们通常不会相互干扰，也不必以互斥的方式执行。互斥有助于防止线程在共享数据时相互干扰，而协作则可以帮助线程一起朝着某个共同的目标努力。当一个线程需要某些数据处于特定状态，而另一个线程负责使数据进入该状态时，合作非常重要。例如生产者/消费者问题：读取线程需要缓冲区处于“非空”状态才可以从缓冲区读取任何数据。如果读取线程发现缓冲区为空，则必须等待。写线程负责用数据填充缓冲区。一旦写入线程完成了写入操作，读取线程便可以进行更多读取操作。有时也称为“等待并通知（Wait and Notify）”或“发出信号并继续（Signal and Continue）”监视器。因为它保留了监视器的所有权，并在需要时继续执行监视器区域。在稍后的某个时间，通知线程释放监视器，并且等待线程恢复拥有该锁。这种合作需要入口集（Entry Set）和等待集（Wait Set）。下面给出的图将帮助您理解这种合作。上图将监视器显示为三个矩形：中心：一个大矩形包含一个线程，即监视器的所有者。左侧：一个小矩形包含入口集（Entry Set）。右侧：另一个小矩形包含等待集（Wait Set）。希望以上讨论将有助于您获得更多见解。欢迎随时提问。学习愉快！说明：本文基于 Java concurrency 基础理论撰写。JDK 不同版本中对于对象锁的具体实现（如偏向锁、轻量级锁等优化）可能存在差异，请以当前官方文档为准。

Java并发性– yield（）和join（）之间的区别
type: item
date: 2026-07-09T04:39:28+00:00
description: Java 并发性：yield() 和 join() 之间的区别多线程长期以来一直是读者非常关注的话题。虽然个人经验表明，真正从事复杂多线程应用程序开发的机会并不多（在过去 7 年中我仅遇到过一次），但熟悉这些概念仍能增强您处理并发问题的信心。此前，我曾讨论过 wait() 和 sleep() 方法之间的差异，本文将重点探讨 join() 和 yield() 方法之间的区别。坦白地说，我在实际生产环境中同时使用这两种方法的场景较少，如果您有任何补充或不同见解，欢迎在评论区提出。Java 线程调度背景Java 虚拟机（JVM）需要在其各个线程之间实施基于优先级的抢占式调度程序（Priority-based Preemptive Scheduler）。这意味着 Java 程序中的每个线程都被分配了一个优先级，该优先级在明确定义的范围内，且开发人员可以更改此优先级。JVM 本身不会更改线程的优先级，即使该线程已经运行了一段时间。优先级值至关重要，因为 JVM 与底层操作系统之间的约定是：操作系统通常必须选择以最高优先级运行 Java 线程。这就是 Java 实现基于优先级调度程序的含义。此调度程序以抢占式（Preemptive）方式实现，意味着当出现更高优先级的线程时，它会中断（抢占）当时运行的更低优先级的线程。然而，与操作系统的契约并非绝对的。这意味着操作系统有时可能选择运行优先级较低的线程。注意：关于多线程的一个常见困惑是“没有保证”。确实，线程调度的具体行为高度依赖于底层操作系统和 JVM 实现。此外，Java 并不强制要求对其线程进行时间分片（Time-slicing），但大多数操作系统都会这样做。这里术语经常混淆：抢占经常与时间片混淆。实际上，抢占仅意味着优先级较高的线程优先于优先级较低的线程运行；当线程具有相同优先级时，它们不会相互抢占。它们通常受时间片限制，但这并非 Java 语言的强制要求。理解线程优先级了解线程优先级是学习多线程的下一个重要步骤，尤其是理解 yield() 的工作方式。以下是关于线程优先级的关键点：未指定优先级时，所有线程均具有正常优先级（Normal Priority）。优先级可以在 1 到 10 之间指定。10 是最高优先级，1 是最低优先级，5 是正常优先级。优先级最高的线程在执行时会被优先调度，但不能保证它一开始就处于运行状态。与正在等待机会的池中的线程相比，当前正在执行的线程始终具有更高的优先级。线程调度程序决定应该执行哪个线程。可使用 t.setPriority() 设置线程的优先级。优先级应该在线程启动（start()）之前设置。可以使用常量 MIN_PRIORITY、MAX_PRIORITY 和 NORM_PRIORITY 设置优先级。在对线程调度和线程优先级有了基本了解后，让我们进入主题。yield() 方法从理论上讲，“屈服”（Yield）意味着放手、放弃或投降。yield() 方法告诉虚拟机，当前线程愿意让出处理器使用权，以便其他线程可以被调度。这表明该线程当前并没有做太重要的事情。注意：这只是一个提示（Hint），并不保证完全有效。yield() 在 Thread.java 中定义如下：/** * A hint to the scheduler that the current thread is willing to yield its current use of a processor. * The scheduler is free to ignore this hint. * Yield is a heuristic attempt to improve relative progression between threads that would otherwise over-utilize a CPU. * Its use should be combined with detailed profiling and benchmarking to ensure that it actually has the desired effect. */ public static native void yield();从上述定义中，我们可以列出以下要点：yield() 是静态方法，也是本地方法（native）。yield() 告诉当前正在执行的线程，给线程池中具有相同优先级的线程一个机会。无法保证 yield() 将使当前正在执行的线程立即变为可运行状态。它只能使线程从运行状态（Running）变为可运行状态（Runnable），而不能处于等待（Waiting）或阻塞（Blocked）状态。yield() 方法示例用法在下面的示例程序中，我创建了两个线程，分别称为生产…

ConcurrentHashMap之实现细节
type: item
date: 2026-07-09T04:38:18+00:00
description: ConcurrentHashMap 之实现细节ConcurrentHashMap 是 Java 5 中引入的支持高并发、高吞吐量的线程安全 HashMap 实现。通过深入阅读源代码，我们可以理解其精巧的实现机制。本文将与大家共享 ConcurrentHashMap 的核心实现细节。实现原理锁分离 (Lock Stripping.html)ConcurrentHashMap 允许多个修改操作并发进行，其关键在于使用了锁分离技术。它使用多个锁来控制对 hash 表不同部分的修改。ConcurrentHashMap 内部使用段（Segment）来表示这些不同的部分，每个段其实就是一个小的 hash table，它们拥有独立的锁。只要多个修改操作发生在不同的段上，它们就可以并发进行。有些方法需要跨段操作，比如 size() 和 containsValue()。它们可能需要锁定整个表而不仅仅是某个段，这需要按顺序锁定所有段，操作完毕后，再按顺序释放所有段的锁。这里“按顺序”非常重要，否则极有可能出现死锁。在 ConcurrentHashMap 内部，段数组是 final 的，并且其成员变量实际上也是 final 的。但是，仅仅是将数组声明为 final 并不保证数组成员也是 final 的，这需要实现上的保证。这可以确保不会出现死锁，因为获得锁的顺序是固定的。不变性在多线程编程中占有重要地位，下文还将谈到。/** * The segments, each of which is a specialized hash table */ final Segment<K,V>[] segments;不变 (Immutable.html) 和易变 (Volatile.html)ConcurrentHashMap 完全允许多个读操作并发进行，读操作并不需要加锁。如果使用传统的技术，如 HashMap 中的实现，如果允许在 hash 链的中间添加或删除元素，读操作不加锁将得到不一致的数据。ConcurrentHashMap 的实现技术是保证 HashEntry 几乎是不可变的。HashEntry 代表每个 hash 链中的一个节点，其结构如下所示：static final class HashEntry<K,V> { final K key; final int hash; volatile V value; final HashEntry<K,V> next; }可以看到，除了 value 不是 final 的，其它值都是 final 的。这意味着不能从 hash 链的中间或尾部添加或删除节点，因为这需要修改 next 引用值，所有的节点修改只能从头部开始。对于 put 操作，可以一律添加到 Hash 链的头部。但是对于 remove 操作，可能需要从中间删除一个节点，这就需要将要删除节点的前面所有节点整个复制一遍，最后一个节点指向要删除结点的下一个结点。这在讲解删除操作时还会详述。为了确保读操作能够看到最新的值，将 value 设置成 volatile，这避免了加锁。哈希与定位为了加快定位段以及段中 hash 槽的速度，每个段 hash 槽的个数都是 2^n，这使得通过位运算就可以定位段和段中 hash 槽的位置。当并发级别为默认值 16 时，也就是段的个数，hash 值的高 4 位决定分配在哪个段中。但是我们也不要忘记《算法导论》给我们的教训：hash 槽的个数不应该是 2^n，这可能导致 hash 槽分配不均，这需要对 hash 值重新再 hash 一次。以下是重新 hash 的算法：private static int hash(int h.html) { // Spread bits to regularize both segment and index locations, // using variant of single-word Wang/Jenkins hash. h += (h << 15.html) ^ 0xffffcd7d; h ^= (h >>> 10.html); h += (h << 3.html); h ^= (h >>> 6.html); h += (h << 2.html) + (h << 14.html); return h ^ (h >>> 16.html); }这是定位段的方法：final Segment<K,V> segmentFor(int hash.html) { return segments[(hash >>> segmentShift.html) & segmentMask]; }数据结构关于 Hash 表的基础数据结构，这里不做过多探讨。Hash 表的一个重要方面就是如何解决 hash…

Java 理论与实践: 并发集合类
type: item
date: 2026-07-09T04:37:52+00:00
description: Java 理论与实践：并发集合类在 Java 类库中，最早出现的关联集合类是 Hashtable，它是 JDK 1.0 的一部分。Hashtable 提供了一种易于使用的、线程安全的关联 Map 功能，这当然非常方便。然而，这种线程安全性是以性能代价换来的——Hashtable 的所有方法都是同步的。此时，无竞争的同步会导致可观的性能损耗。Hashtable 的后继者 HashMap 作为 JDK 1.2 集合框架的一部分出现，它通过提供一个不同步的基类和一个同步包装器 Collections.synchronizedMap，解决了线程安全性问题。通过将基本功能与线程安全性分离，Collections.synchronizedMap 允许需要同步的用户获得同步保障，而不需要同步的用户则不必为此付出代价。Hashtable 和 synchronizedMap 所采取的同步方法（即同步 Hashtable 中或同步 Map 包装器对象中的每个方法）主要有两个不足。首先，这种方法对于可伸缩性是一种障碍，因为一次只能有一个线程可以访问 Hash 表。同时，这样仍不足以提供真正的线程安全性，许多常见的复合操作仍然需要额外的同步。虽然诸如 get() 和 put() 之类的简单操作可以在不需要额外同步的情况下安全地完成，但还是有一些常见的操作序列，例如迭代或者 put-if-absent（若 absent 则 put），需要外部同步以避免数据争用。有条件的线程安全性同步的集合包装器 synchronizedMap 和 synchronizedList，有时也被称作有条件地线程安全——所有单个的操作都是线程安全的，但是多个操作组成的操作序列却可能导致数据争用，因为在操作序列中控制流取决于前面操作的结果。清单 1 中第一片段展示了常见的 put-if-absent 语句块——如果一个条目不在 Map 中，那么添加这个条目。不幸的是，在 containsKey() 方法返回到 put() 方法被调用这段时间内，可能会有另一个线程也插入一个带有相同键的值。如果您想确保只有一次插入，您需要用一个对 Map m 进行同步的同步块将这一对语句包装起来。清单 1 中其他的例子与迭代有关。在第一个例子中，List.size() 的结果在循环的执行期间可能会变得无效，因为另一个线程可以从这个列表中删除条目。如果时机不当，在刚好进入循环的最后一次迭代之后有一个条目被另一个线程删除了，则 List.get() 将返回 null，而 doSomething() 则很可能会抛出一个 NullPointerException 异常。那么，采取什么措施才能避免这种情况呢？如果当您正在迭代一个 List 时另一个线程也可能正在访问这个 List，那么在进行迭代时您必须使用一个 synchronized 块将这个 List 包装起来，在 List 上同步，从而锁住整个 List。这样做虽然解决了数据争用问题，但是在并发性方面付出了更多的代价，因为在迭代期间锁住整个 List 会阻塞其他线程，使它们在很长一段时间内不能访问这个列表。集合框架引入了迭代器，用于遍历一个列表或者其他集合，从而优化了对一个集合中的元素进行迭代的过程。然而，在 java.util 集合类中实现的迭代器极易崩溃，也就是说，如果在一个线程正在通过一个 Iterator 遍历集合时，另一个线程也来修改这个集合，那么接下来的 Iterator.hasNext() 或 Iterator.next() 调用将抛出 ConcurrentModificationException 异常。就拿刚才这个例子来讲，如果想要防止出现 ConcurrentModificationException 异常，那么当您正在进行迭代时，您必须使用一个在 List l 上同步的 synchronized 块将该 List 包装起来，从而锁住整个 List。（或者，您也可以调用 List.toArray()，在不同步的情况下对数组进行迭代，但是如果列表比较大的话这样做代价很高）。清单 1. 同步 Map 中的常见竞争条件Map m = Collections.synchronizedMap(new HashMap(.html)); List l = Collections.synchronizedList(new ArrayList(.html)); // put-if-absent idiom -- contains a race condition // may require external synchronization if (!map.containsKey(key.html)) map.put(key, value.htm…

Java中sleep（）和wait（）之间的区别
type: item
date: 2026-07-09T04:37:47+00:00
description: Java 中 sleep() 和 wait() 之间的区别本文旨在探讨 Java 中 sleep() 和 wait() 方法之间的区别，分析何时使用哪种方法，以及它们对 Java 并发编程带来的影响。1. 核心机制讨论sleep() 是一种用于暂停当前线程执行的方法，可以使线程暂停指定的毫秒数。而在使用 wait() 方法的情况下，线程会进入等待状态，直到其他线程调用该对象的 notify() 或 notifyAll() 方法，它才会被唤醒并重新竞争锁。两者的主要区别在于锁的处理机制：sleep() 不会释放锁（监视器），而 wait() 会释放锁。 wait() 通常用于线程间通信，而 sleep() 通常用于引入执行暂停或延时。Thread.sleep() 会将当前线程发送到 TIMED_WAITING 状态一段时间。该线程会保留已获取的监视器，即如果该线程当前在某个 synchronized 块或方法中，则没有其他线程可以进入该块或方法。如果另一个线程调用 t.interrupt()，它将唤醒睡眠中的线程并抛出 InterruptedException。需要注意的是，sleep() 是一个 静态方法，这意味着它始终会影响当前线程（正在执行 sleep 方法的线程）。一个常见的错误是调用 t.sleep()，其中 t 是另一个线程对象。即使这样写，当前线程也会进入休眠状态，而不是线程 t。推荐阅读：使用 wait() 和 notify()2. 代码示例sleep() 示例在 synchronized 块中调用 sleep()，锁不会被释放：synchronized(LOCK.html) { Thread.sleep(1000.html); // LOCK 依然被当前线程持有 }wait() 示例在 synchronized 块中调用 wait()，锁会被释放：synchronized(LOCK.html) { LOCK.wait(); // LOCK 被释放，直到被唤醒并重新获取锁 }推荐阅读：yield() 和 join() 之间的区别3. 对比总结简而言之，我们将以上要点进行分类总结，以便记忆和理解。特性sleep()wait()调用对象调用 Thread 类（静态方法）；始终影响当前正在执行的线程。调用 Object 类；当前线程必须在锁对象上同步。同步要求不需要同步上下文，但在同步块中使用时会持有锁。必须在 synchronized 同步块或方法中调用，否则抛出 IllegalMonitorStateException。锁行为不释放锁。休眠期间，其他线程无法进入同一对象的同步块。释放锁。允许其他线程有机会执行同步代码。唤醒条件直到指定时间到期，或调用 interrupt() 中断。直到其他线程调用对象的 notify() 或 notifyAll()。主要用途用于时间同步、暂停执行或模拟延时。用于多线程间的协作与通信。希望以上信息能为您的知识库增加价值。学习愉快！

Java比较和交换示例– CAS算法
type: item
date: 2026-07-09T04:37:13+00:00
description: 1. 乐观锁与悲观锁 (Optimistic and Pessimistic Locking.html)传统的锁定机制（例如 Java 中的 synchronized 关键字）被称为悲观锁（Pessimistic Locking）技术。它要求线程在访问共享资源前必须先获得锁，确保没有其他线程会干扰操作（即锁定对象），然后才允许访问实例或方法。这就好比说：“请先关上门，否则其他干扰者会进来重新整理您的东西。”尽管上述方法是安全的且确实有效，但它会给应用程序的性能带来显著损失。原因很简单：等待锁的线程无法执行任何操作，直到它们有机会获取锁并执行受保护的操作为止。存在一种在性能上更有效且本质上是乐观的方法。使用这种方法，线程可以直接进行更新，假设在操作期间不会发生干扰。此方法依靠冲突检测来确定更新期间是否存在来自其他线程的干扰；在这种情况下，操作将失败，线程可以选择重试（或不重试）。乐观锁的方法就像那句老话所说：“请求原谅比获得许可更容易（Easier to ask forgiveness than permission）”，这里的“容易”意味着“更高效”。比较并交换（Compare-And-Swap, CAS） 就是这种乐观方法的一个典型例子，我们将在下面详细讨论。2. 比较并交换算法 (Compare and Swap Algorithm.html)CAS 算法将存储位置的内容与给定值进行比较，并且只有当它们相同时，才将该存储位置的内容修改为给定的新值。这是作为单个原子操作（Atomic Operation）完成的。原子性保证了新值是根据最新信息计算的；如果与此同时值已被另一个线程更新，则写入将失败。操作结果必须表明是否执行了替换；这可以通过简单的布尔响应（此变量通常称为“比较设置”）来完成，也可以通过返回从内存位置读取的值（而不是写入该值）来完成。CAS 操作包含三个操作数：内存位置 V：必须替换值的存储位置。预期原值 A：线程上次读取的旧值。新值 B：应该写在 V 上的新值。CAS 的逻辑可以表述为：“我认为 V 应该具有值 A；如果可以，则将 B 放在此处；否则不要更改它，但要告诉我操作失败了。”CAS 是一种乐观技术，它希望成功进行更新，并且如果自从上次检查变量以来另一个线程更新了该变量，它可以检测到失败。3. Java CAS 示例 (Java Compare and Swap Example.html)让我们通过一个例子来了解整个过程。假设 V 是存储值 10 的内存位置。有多个线程想要递增此值并将递增后的值用于其他操作，这是一种非常实际的场景。让我们分步分解整个 CAS 操作：1) 线程准备阶段线程 1 和线程 2 都想递增该值。它们都读取了当前值 10，并计算出目标值为 11。当前内存值：V = 10线程预期值：A = 10线程新值：B = 112) 线程 1 执行 CAS线程 1 首先发起请求，将 V 与它的预期值 A 进行比较：状态：V = 10, A = 10, B = 11if (A == V.html) { V = B; // 更新成功 } else { // 操作失败 return V; // 返回当前值 }显然，V 的值等于 A，因此 V 将被覆盖为 11，操作成功。3) 线程 2 执行 CAS线程 2 随后到来，尝试执行与线程 1 相同的操作：状态：V = 11 (已被线程 1 修改.html), A = 10 (线程 2 旧的预期值.html), B = 11if (A == V.html) { V = B; } else { // 操作失败 return V; }4) 线程 2 重试在这种情况下，V (11.html) 不等于 A (10.html)，因此不替换值，操作失败。线程 2 获取到当前最新值 11，再次使用新值重试此操作：新状态：V = 11, A = 11, B = 12这一次，条件得到满足，增量值 12 被成功写入，操作返回成功。总结总而言之，当多个线程尝试使用 CAS 同时更新同一变量时，只有一个线程会获胜并更新该变量的值，其余线程会发现操作失败。但是，失败者并不会因为线程阻塞而受到惩罚。它们可以自由地重试该操作（自旋），或者根据业务逻辑放弃操作。这就是 Java 中原子操作支持背后的简单但重要的概念。说明：本文基于 Java 5 引入的 java.util.concurrent.atomic 包机制编写。CAS 算法是现代 Java 并发编程（如 AtomicInteger、AtomicLong 等类）的底层基础，适用于高并发场景下的无锁编程。

Java并发性–线程安全性？
type: item
date: 2026-07-09T04:37:05+00:00
description: Java 并发性 – 线程安全性？定义线程安全性（Thread Safety）出奇地棘手。快速搜索会发现许多类似的“定义”：线程安全代码是即使许多线程同时执行也可以运行的代码。如果一段代码仅以保证多个线程同时安全执行的方式操作共享数据结构，则它是线程安全的。此外还有更多类似的定义。你是否觉得上述定义实际上并未传达任何有意义的信息，甚至增加了一些混乱？尽管不能排除这些定义，因为它们没有错，但事实是它们没有提供任何实际帮助或观点。我们如何区分线程安全的类和不安全的类？我们甚至不清楚“安全”究竟意味着什么。什么是线程安全的正确性？线程安全性任何合理定义的核心是正确性（Correctness）的概念。因此，在了解线程安全性之前，我们应该首先了解“正确性”。正确性表示一个类符合其规范（Specification）。你将同意，良好的类规范将在任何给定时间包含有关类状态的所有信息，并且如果对它执行了某些操作，则它是后置条件（Postcondition）。由于我们经常没有为类编写足够的规范，怎么可能知道它们是正确的？我们不能。但是，一旦我们说服自己“代码可以工作”，那也不会阻止我们继续使用它们。这种“代码信心”与我们许多人接近正确性的程度差不多。乐观地将“正确性”定义为可以识别的事物之后，我们现在可以以一种非循环的方式定义线程安全性：当一个类在从多个线程访问时继续正确运行时，它是线程安全的。如果一个类在从多个线程访问时能正确运行，则无论运行时环境如何调度或交织那些线程的执行，并且在调用代码部分没有任何其他同步或其他协调的情况下，如果它行为正确，则该线程是安全的。如果此处对“正确性”的宽松使用使您感到困扰，则您可能更愿意将线程安全类认为是在并发环境中比在单线程环境中不会损坏的类。线程安全类封装了任何需要的同步，因此客户端不需要提供自己的同步。示例：无状态 Servlet线程安全类的一个很好的例子是 Java Servlet，它没有字段和引用，也没有其他类的字段等。它们是无状态的（Stateless）。public class StatelessFactorizer implements Servlet { public void service(ServletRequest req, ServletResponse resp.html) { BigInteger i = extractFromRequest(req.html); BigInteger[] factors = factor(i.html); encodeIntoResponse(resp, factors.html); } }特定计算的中间状态仅存在于局部变量中，这些局部变量存储在线程的栈（Stack）中，并且只有执行线程才能访问。一个线程访问 StatelessFactorizer 不会影响另一个线程访问 StatelessFactorizer 的结果；因为两个线程不共享状态，所以好像它们正在访问不同的实例。由于线程访问无状态对象的动作不会影响其他线程中操作的正确性，因此无状态对象是线程安全的。这就是关于什么是线程安全的小而重要的概念的全部内容。学习愉快！

HashMap vs ConcurrentHashMap — 示例及Iterator探秘
type: item
date: 2026-07-09T04:35:49+00:00
description: 引言作为一名 Java 开发人员，你一定对 ConcurrentModificationException 不陌生。这是一个在使用迭代器遍历集合对象时，因并发修改集合对象而引发的异常。实际上，Java 集合框架是 迭代器设计模式 的一个典型实现。Java 1.5 引入了 java.util.concurrent 包，其中 Collection 类 的实现允许在运行过程中修改集合对象。ConcurrentHashMap 是一个与 HashMap 很相似的类，但它支持在遍历过程中安全地修改集合对象。让我们通过一个简单的程序来深入理解两者的区别。示例代码ConcurrentHashMapExample.javapackage com.journaldev.util; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class ConcurrentHashMapExample { public static void main(String[] args.html) { // ConcurrentHashMap Map<String, String> myMap = new ConcurrentHashMap<String, String>(); myMap.put("1", "1".html); myMap.put("2", "1".html); myMap.put("3", "1".html); myMap.put("4", "1".html); myMap.put("5", "1".html); myMap.put("6", "1".html); System.out.println("ConcurrentHashMap before iterator: " + myMap.html); Iterator<String> it = myMap.keySet().iterator(); while (it.hasNext(.html)) { String key = it.next(); if (key.equals("3".html)) myMap.put(key + "new", "new3".html); } System.out.println("ConcurrentHashMap after iterator: " + myMap.html); // HashMap myMap = new HashMap<String, String>(); myMap.put("1", "1".html); myMap.put("2", "1".html); myMap.put("3", "1".html); myMap.put("4", "1".html); myMap.put("5", "1".html); myMap.put("6", "1".html); System.out.println("HashMap before iterator: " + myMap.html); Iterator<String> it1 = myMap.keySet().iterator(); while (it1.hasNext(.html)) { String key = it1.next(); if (key.equals("3".html)) myMap.put(key + "new", "new3".html); } System.out.println("HashMap after iterator: " + myMap.html); } }运行结果与分析当我们试着运行上面的程序，输出如下：ConcurrentHashMap before iterator: {1=1, 5=1, 6=1, 3=1, 4=1, 2=1} ConcurrentHashMap after iterator: {1=1, 3new=new3, 5=1, 6=1, 3=1, 4=1, 2=1} HashMap before iterator: {3=1, 2=1, 1=1, 6=1, 5=1, 4=1} Exception in thread "main" java.util.ConcurrentModificationException at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793.html) at java.util.HashMap$KeyIterator.next(Hash…

Java Multi-threading Evolution and Topics
type: item
date: 2026-07-09T04:34:11+00:00
description: JDK 版本演进中的多线程概念JDK 1.x：初始阶段在 JDK 1.x 初始发行版中，提供的多线程相关类非常有限。具体包含以下核心类与接口：java.lang.Threadjava.lang.ThreadGroupjava.lang.Runnablejava.lang.Processjava.lang.ThreadDeath以及一些异常类，例如：java.lang.IllegalMonitorStateExceptionjava.lang.IllegalStateExceptionjava.lang.IllegalThreadStateException当时尚未提供丰富的同步集合框架（仅有如 java.util.Hashtable 等基础类）。JDK 1.2 至 1.4：平稳过渡JDK 1.2 和 JDK 1.3 在与多线程相关的特性上没有明显变化（如有遗漏，敬请指正）。JDK 1.4 中，JVM 层面几乎没有变更。虽然可以通过特定调用挂起或恢复多个线程，但并未出现重大的 API 更改。JDK 1.5：并发包的里程碑JDK 1.5 是继 JDK 1.x 之后的第一个重要版本，引入了多个并发实用工具。Executor、Semaphore、Mutex、Barrier、CountDownLatch、并发集合（Concurrent Collections）以及阻塞队列（Blocking Queues）均包含在此版本中。Java 多线程应用领域的最大变革发生在此版本中。阅读此链接中的全部更改集：Java 5 Concurrency OverviewJDK 1.6：优化与改进与单纯的 API 升级相比，JDK 1.6 不仅仅是平台修复。因此，JDK 1.6 中也出现了一些新的变化与性能优化。JDK 1.7：Fork/Join 框架JDK 1.7 添加了对 ForkJoinPool 的支持，该框架通过实施工作窃取（Work-Stealing）技术来最大化吞吐量。此外，还添加了 Phaser 类。JDK 1.8：Lambda 与流式并发JDK 1.8 以 Lambda 表达式而闻名，同时也包含了一些并发特性的更新。在 java.util.concurrent 包中添加了两个新接口和四个新类，例如 CompletableFuture 和 CompletionException。集合框架在 Java 8 中进行了重大修改，基于新添加的流（Stream）工具和 Lambda 表达式引入了聚合操作；这导致几乎所有 Collection 类中都添加了大量方法，并发集合类中也相应增加了很多方法。说明：本文内容主要涵盖至 JDK 1.8 版本。Java 后续版本（如 JDK 11、17、21 等）在并发编程领域仍有持续更新与改进，请参考官方最新文档以获取时效性信息。

Java Callable Future Example
type: item
date: 2026-07-09T04:33:50+00:00
description: Java 并发执行框架的优势之一是支持运行并发任务，并在任务处理后返回单个结果。Java 并发 API 通过 Callable 和 Future 两个接口实现了这一功能。1. Java Callable 和 Future 接口1.1 Callable 接口Callable 接口定义了 call() 方法，我们需要在该方法中实现具体的任务逻辑。Callable 是一个泛型接口，这意味着我们必须指定 call() 方法将返回的数据类型。1.2 Future 接口Future 接口提供了获取 Callable 任务生成结果以及管理任务状态的方法。2. Java Callable 与 Future 示例在本示例中，我们创建了一个实现 Callable 接口的 FactorialCalculator 类。这意味着我们将重写其 call() 方法，并在计算完成后从该方法返回结果。随后，主程序可以通过保存的 Future 引用检索此结果。// FactorialCalculator.java import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; public class FactorialCalculator implements Callable<Integer> { private Integer number; public FactorialCalculator(Integer number.html) { this.number = number; } @Override public Integer call() throws Exception { int result = 1; if ((number == 0.html) || (number == 1.html)) { result = 1; } else { for (int i = 2; i <= number; i++.html) { result *= i; TimeUnit.MILLISECONDS.sleep(20.html); } } System.out.println("Result for number - " + number + " -> " + result.html); return result; } }现在，让我们使用两个线程和 4 个数字来测试上面的阶乘计算器。// CallableExample.java package com.howtodoinjava.demo.multithreading; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class CallableExample { public static void main(String[] args.html) { ThreadPoolExecutor executor = (ThreadPoolExecutor.html) Executors.newFixedThreadPool(2.html); List<Future<Integer>> resultList = new ArrayList<>(); Random random = new Random(); for (int i = 0; i < 4; i++.html) { Integer number = random.nextInt(10.html); FactorialCalculator calculator = new FactorialCalculator(number.html); Future<Integer> result = executor.submit(calculator.html); resultList.add(result.html); } for (Future<Integer> future : resultList.html) { try { System.out.printl…