Difference between lock and monitor – Java Concurrency
编者注：本文为历史博文归档；涉及 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() 之间的区别多线程长期以来一直是读者非常关注的话题。虽然个人经验表明，真正从事复杂多线程应用程序开发的机会并不多（在过去 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() 方法示例用法在下面的示例程序中，我创建了两个线程，分别称为生产者（Producer）和消费者（Consumer）。生产者设置为最小优先级，而消费者设置为最大优先级。我将分别在有无 Thread.yield() 调用的情况下运行以下代码。没有 yield() 时：尽管输出有时会更改，但通常首先打印所有消费者行，然后才打印所有生产者行（因为消费者优先级高）。使用 yield() 方法：两者交替打印，几乎总是将机会传递给另一线程。package com.daicy.concurrency.thread; /** * @author: create by daichangya * @version: v1.0 * @description: com.daicy.concurrency.thread * @date: 20-4-17 */ public class YieldTest { public static void main(String[] args) { Thread producer = new Producer(); Thread consumer = new Consumer(); producer.setPriority(Thread.MIN_PRIORITY); // 最小优先级 consumer.setPriority(Thread.MAX_PRIORITY); // 最大优先级 producer.start(); consumer.start(); } } class Producer extends Thread { public void run() { for (int i = 0; i < 5; i++) { System.out.println("I am Producer : Produced Item " + i); Thread.yield(); } } } class Consumer extends Thread { public void run() { for (int i = 0; i < 5; i++) { System.out.println("I am Consumer : Consumed Item " + i); Thread.yield(); } } }不使用 yield() 方法的输出示例：I am Producer : Produced Item 0 I am Producer : Produced Item 1 I am Producer : Produced Item 2 I am Producer : Produced Item 3 I am Producer : Produced Item 4 I am Consumer : Consumed Item 0 I am Consumer : Consumed Item 1 I am Consumer : Consumed Item 2 I am Consumer : Consumed Item 3 I am Consumer : Consumed Item 4使用 yield() 方法的输出示例：I am Producer : Produced Item 0 I am Consumer : Consumed Item 0 I am Producer : Produced Item 1 I am Consumer : Consumed Item 1 I am Producer : Produced Item 2 I am Consumer : Consumed Item 2 I am Producer : Produced Item 3 I am Consumer : Consumed Item 3 I am Producer : Produced Item 4 I am Consumer : Consumed Item 4join() 方法线程实例的 join() 方法可用于将一个线程的执行“联接”到另一个线程的结束。这意味着调用 join() 的线程将等待被调用线程完成执行后才继续运行。如果在 Thread 实例上调用 join()，则当前正在运行的线程将阻塞，直到该 Thread 实例完成执行。// 等待此线程死亡 public final void join() throws InterruptedException在 join() 中设置超时参数，将使特定超时时间后 join() 的等待效果失效。当达到超时时，主线程和被 join 的线程执行的可能性同等。但是，与 sleep() 一样，join() 的运行时间也取决于操作系统，因此您不应假定 join() 会完全按照您指定的时间等待。像 sleep() 一样，join() 通过抛出 InterruptedException 来响应中断。join() 方法示例用法package com.daicy.concurrency.thread; /** * @author: create by daichangya * @version: v1.0 * @description: com.daicy.concurrency.thread * @date: 20-4-17 */ public class JoinTest { public static void main(String[] args) throws InterruptedException { Thread t = new Thread(new Runnable() { public void run() { System.out.println("First task started"); System.out.println("Sleeping for 2 seconds"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("First task completed"); } }); Thread t1 = new Thread(new Runnable() { public void run() { System.out.println("Second task completed"); } }); t.start(); t.join(); // 主线程等待 t 执行完毕 t1.start(); } }输出：First task started Sleeping for 2 seconds First task completed Second task completed仅此一个很小但很重要的概念。欢迎在评论部分分享您的想法。学习愉快！说明：本文基于 Java 基础线程 API 编写。在现代 Java 并发编程中，建议优先使用 java.util.concurrent 包下的高级工具类（如 ExecutorService、CountDownLatch 等）来管理线程生命周期和同步，而非直接操作底层 Thread 类。
ConcurrentHashMap 之实现细节ConcurrentHashMap 是 Java 5 中引入的支持高并发、高吞吐量的线程安全 HashMap 实现。通过深入阅读源代码，我们可以理解其精巧的实现机制。本文将与大家共享 ConcurrentHashMap 的核心实现细节。实现原理锁分离 (Lock Stripping)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) 和易变 (Volatile)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) { // Spread bits to regularize both segment and index locations, // using variant of single-word Wang/Jenkins hash. h += (h << 15) ^ 0xffffcd7d; h ^= (h >>> 10); h += (h << 3); h ^= (h >>> 6); h += (h << 2) + (h << 14); return h ^ (h >>> 16); }这是定位段的方法：final Segment<K,V> segmentFor(int hash) { return segments[(hash >>> segmentShift) & segmentMask]; }数据结构关于 Hash 表的基础数据结构，这里不做过多探讨。Hash 表的一个重要方面就是如何解决 hash 冲突，ConcurrentHashMap 和 HashMap 使用相同的方式，都是将 hash 值相同的节点放在一个 hash 链中。与 HashMap 不同的是，ConcurrentHashMap 使用多个子 Hash 表，也就是段（Segment）。下面是 ConcurrentHashMap 的数据成员：public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable { /** * Mask value for indexing into segments. The upper bits of a * key's hash code are used to choose the segment. */ final int segmentMask; /** * Shift value for indexing within segments. */ final int segmentShift; /** * The segments, each of which is a specialized hash table */ final Segment<K,V>[] segments; }所有的成员都是 final 的，其中 segmentMask 和 segmentShift 主要是为了定位段，参见上面的 segmentFor 方法。每个 Segment 相当于一个子 Hash 表，它的数据成员如下：static final class Segment<K,V> extends ReentrantLock implements Serializable { private static final long serialVersionUID = 2249069246763182397L; /** * The number of elements in this segment's region. */ transient volatile int count; /** * Number of updates that alter the size of the table. This is * used during bulk-read methods to make sure they see a * consistent snapshot: If modCounts change during a traversal * of segments computing size or checking containsValue, then * we might have an inconsistent view of state so (usually) * must retry. */ transient int modCount; /** * The table is rehashed when its size exceeds this threshold. * (The value of this field is always (int)(capacity * * loadFactor) .) */ transient int threshold; /** * The per-segment table. */ transient volatile HashEntry<K,V>[] table; /** * The load factor for the hash table. Even though this value * is same for all segments, it is replicated to avoid needing * links to outer object. * @serial */ final float loadFactor; }count 用来统计该段数据的个数，它是 volatile，用来协调修改和读取操作，以保证读取操作能够读取到几乎最新的修改。协调方式是这样的：每次修改操作做了结构上的改变，如增加/删除节点（修改节点的值不算结构上的改变），都要写 count 值；每次读取操作开始都要读取 count 的值。这利用了 Java 5 中对 volatile 语义的增强，对同一个 volatile 变量的写和读存在 happens-before 关系。modCount 统计段结构改变的次数，主要是为了检测对多个段进行遍历过程中某个段是否发生改变，在讲述跨段操作时还会详述。threshold 用来表示需要进行 rehash 的界限值。table 数组存储段中节点，每个数组元素是个 hash 链，用 HashEntry 表示。table 也是 volatile，这使得能够读取到最新的 table 值而不需要同步。loadFactor 表示负载因子。实现细节修改操作先来看下删除操作 remove(key)。public V remove(Object key) { int hash = hash(key.hashCode()); return segmentFor(hash).remove(key, hash, null); }整个操作是先定位到段，然后委托给段的 remove 操作。当多个删除操作并发进行时，只要它们所在的段不相同，它们就可以同时进行。下面是 Segment 的 remove 方法实现：V remove(Object key, int hash, Object value) { lock(); try { int c = count - 1; HashEntry<K,V>[] tab = table; int index = hash & (tab.length - 1); HashEntry<K,V> first = tab[index]; HashEntry<K,V> e = first; while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue = null; if (e != null) { V v = e.value; if (value == null || value.equals(v)) { oldValue = v; // All entries following removed node can stay // in list, but all preceding ones need to be // cloned. ++modCount; HashEntry<K,V> newFirst = e.next; for (HashEntry<K,V> p = first; p != e; p = p.next) newFirst = new HashEntry<K,V>(p.key, p.hash, newFirst, p.value); tab[index] = newFirst; count = c; // write-volatile } } return oldValue; } finally { unlock(); } }整个操作是在持有段锁的情况下执行的，空白行之前的行主要是定位到要删除的节点 e。接下来，如果不存在这个节点就直接返回 null，否则就要将 e 前面的结点复制一遍，尾结点指向 e 的下一个结点。e 后面的结点不需要复制，它们可以重用。下面是个示意图：删除元素之前：删除元素 3 之后：第二个图其实有点问题，复制的结点中应该是值为 2 的结点在前面，值为 1 的结点在后面，也就是刚好和原来结点顺序相反，还好这不影响我们的讨论。整个 remove 实现并不复杂，但是需要注意如下几点：当要删除的结点存在时，删除的最后一步操作要将 count 的值减一。这必须是最后一步操作，否则读取操作可能看不到之前对段所做的结构性修改。remove 执行的开始就将 table 赋给一个局部变量 tab，这是因为 table 是 volatile 变量，读写 volatile 变量的开销很大。编译器也不能对 volatile 变量的读写做任何优化，直接多次访问非 volatile 实例变量没有多大影响，编译器会做相应优化。接下来看 put 操作，同样地 put 操作也是委托给段的 put 方法。下面是段的 put 方法：V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); try { int c = count; if (c++ > threshold) // ensure capacity rehash(); HashEntry<K,V>[] tab = table; int index = hash & (tab.length - 1); HashEntry<K,V> first = tab[index]; HashEntry<K,V> e = first; while (e != null && (e.hash != hash || !key.equals(e.key))) e = e.next; V oldValue; if (e != null) { oldValue = e.value; if (!onlyIfAbsent) e.value = value; } else { oldValue = null; ++modCount; tab[index] = new HashEntry<K,V>(key, hash, first, value); count = c; // write-volatile } return oldValue; } finally { unlock(); } }该方法也是在持有段锁的情况下执行的，首先判断是否需要 rehash，需要就先 rehash。接着是找是否存在同样一个 key 的结点，如果存在就直接替换这个结点的值。否则创建一个新的结点并添加到 hash 链的头部，这时一定要修改 modCount 和 count 的值，同样修改 count 的值一定要放在最后一步。put 方法调用了 rehash 方法，rehash 方法实现得也很精巧，主要利用了 table 的大小为 2^n，这里就不介绍了。修改操作还有 putAll 和 replace。putAll 就是多次调用 put 方法。replace 甚至不用做结构上的更改，实现要比 put 和 remove 简单得多，理解了 put 和 remove，理解 replace 就不在话下了，这里也不介绍了。获取操作首先看下 get 操作，同样 ConcurrentHashMap 的 get 操作是直接委托给 Segment 的 get 方法，直接看 Segment 的 get 方法：V get(Object key, int hash) { if (count != 0) { // read-volatile HashEntry<K,V> e = getFirst(hash); while (e != null) { if (e.hash == hash && key.equals(e.key)) { V v = e.value; if (v != null) return v; return readValueUnderLock(e); // recheck } e = e.next; } } return null; }get 操作不需要锁。第一步是访问 count 变量，这是一个 volatile 变量，由于所有的修改操作在进行结构修改时都会在最后一步写 count 变量，通过这种机制保证 get 操作能够得到几乎最新的结构更新。对于非结构更新，也就是结点值的改变，由于 HashEntry 的 value 变量是 volatile 的，也能保证读取到最新的值。接下来就是对 hash 链进行遍历找到要获取的结点，如果没有找到，直接返回 null。对 hash 链进行遍历不需要加锁的原因在于链指针 next 是 final 的。但是头指针却不是 final 的，这是通过 getFirst(hash) 方法返回，也就是存在 table 数组中的值。这使得 getFirst(hash) 可能返回过时的头结点，例如，当执行 get 方法时，刚执行完 getFirst(hash) 之后，另一个线程执行了删除操作并更新头结点，这就导致 get 方法中返回的头结点不是最新的。这是可以允许的，通过对 count 变量的协调机制，get 能读取到几乎最新的数据，虽然可能不是最新的。要得到最新的数据，只有采用完全的同步。最后，如果找到了所求的结点，判断它的值如果非空就直接返回，否则在有锁的状态下再读一次。这似乎有些费解，理论上结点的值不可能为空，这是因为 put 的时候就进行了判断，如果为空就要抛 NullPointerException。空值的唯一源头就是 HashEntry 中的默认值，因为 HashEntry 中的 value 不是 final 的，非同步读取有可能读取到空值。仔细看下 put 操作的语句：tab[index] = new HashEntry。V readValueUnderLock(HashEntry<K,V> e) { lock(); try { return e.value; } finally { unlock(); } }另一个操作是 containsKey，这个实现就要简单得多了，因为它不需要读取值：boolean containsKey(Object key, int hash) { if (count != 0) { // read-volatile HashEntry<K,V> e = getFirst(hash); while (e != null) { if (e.hash == hash && key.equals(e.key)) return true; e = e.next; } } return false; }跨段操作有些操作需要涉及到多个段，比如说 size(), containsValue()。先来看下 size() 方法：public int size() { final Segment<K,V>[] segments = this.segments; long sum = 0; long check = 0; int[] mc = new int[segments.length]; // Try a few times to get accurate count. On failure due to // continuous async changes in table, resort to locking. for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) { check = 0; sum = 0; int mcsum = 0; for (int i = 0; i < segments.length; ++i) { sum += segments[i].count; mcsum += mc[i] = segments[i].modCount; } if (mcsum != 0) { for (int i = 0; i < segments.length; ++i) { check += segments[i].count; if (mc[i] != segments[i].modCount) { check = -1; // force retry break; } } } if (check == sum) break; } if (check != sum) { // Resort to locking all segments sum = 0; for (int i = 0; i < segments.length; ++i) segments[i].lock(); for (int i = 0; i < segments.length; ++i) sum += segments[i].count; for (int i = 0; i < segments.length; ++i) segments[i].unlock(); } if (sum > Integer.MAX_VALUE) return Integer.MAX_VALUE; else return (int)sum; }size 方法主要思路是先在没有锁的情况下对所有段大小求和，如果不能成功（这是因为遍历过程中可能有其它线程正在对已经遍历过的段进行结构性更新），最多执行 RETRIES_BEFORE_LOCK 次，如果还不成功就在持有所有段锁的情况下再对所有段大小求和。在没有锁的情况下主要是利用 Segment 中的 modCount 进行检测，在遍历过程中保存每个 Segment 的 modCount，遍历完成之后再检测每个 Segment 的 modCount 有没有改变，如果有改变表示有其它线程正在对 Segment 进行结构性并发更新，需要重新计算。其实这种方式是存在问题的，在第一个内层 for 循环中，在这两条语句 sum += segments[i].count; 和 mcsum += mc[i] = segments[i].modCount; 之间，其它线程可能正在对 Segment 进行结构性的修改，导致 segments[i].count 和 segments[i].modCount 读取的数据并不一致。这可能使 size() 方法返回任何时候都不曾存在的大小，很奇怪 javadoc 居然没有明确标出这一点，可能是因为这个时间窗口太小了吧。size() 的实现还有一点需要注意，必须要先访问 segments[i].count，才能访问 segments[i].modCount，这是因为 segment[i].count 是对 volatile 变量的访问，接下来 segments[i].modCount 才能得到几乎最新的值。这点在 containsValue 方法中得到了淋漓尽致的展现：public boolean containsValue(Object value) { if (value == null) throw new NullPointerException(); // See explanation of modCount use above final Segment<K,V>[] segments = this.segments; int[] mc = new int[segments.length]; // Try a few times without locking for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) { int sum = 0; int mcsum = 0; for (int i = 0; i < segments.length; ++i) { int c = segments[i].count; mcsum += mc[i] = segments[i].modCount; if (segments[i].containsValue(value)) return true; } boolean cleanSweep = true; if (mcsum != 0) { for (int i = 0; i < segments.length; ++i) { int c = segments[i].count; if (mc[i] != segments[i].modCount) { cleanSweep = false; break; } } } if (cleanSweep) return false; } // Resort to locking all segments for (int i = 0; i < segments.length; ++i) segments[i].lock(); boolean found = false; try { for (int i = 0; i < segments.length; ++i) { if (segments[i].containsValue(value)) { found = true; break; } } } finally { for (int i = 0; i < segments.length; ++i) segments[i].unlock(); } return found; }同样注意内层的第一个 for 循环，里面有语句 int c = segments[i].count; 但是 c 却从来没有被使用过，即使如此，编译器也不能做优化将这条语句去掉，因为存在对 volatile 变量 count 的读取，这条语句存在的唯一目的就是保证 segments[i].modCount 读取到几乎最新的值。关于 containsValue 方法的其它部分就不分析了，它和 size 方法差不多。跨段方法中还有一个 isEmpty() 方法，其实现比 size() 方法还要简单，也不介绍了。最后简单地介绍下迭代方法，如 keySet(), values(), entrySet() 方法，这些方法都返回相应的迭代器，所有迭代器都继承于 HashIterator 类，里实现了主要的方法。其结构是：abstract class HashIterator { int nextSegmentIndex; int nextTableIndex; HashEntry<K,V>[] currentTable; HashEntry<K, V> nextEntry; HashEntry<K, V> lastReturned; }nextSegmentIndex 是段的索引，nextTableIndex 是 nextSegmentIndex 对应段中 hash 链的索引，currentTable 是 nextSegmentIndex 对应段的 table。调用 next 方法时主要是调用了 advance 方法：final void advance() { if (nextEntry != null && (nextEntry = nextEntry.next) != null) return; while (nextTableIndex >= 0) { if ( (nextEntry = currentTable[nextTableIndex--]) != null) return; } while (nextSegmentIndex >= 0) { Segment<K,V> seg = segments[nextSegmentIndex--]; if (seg.count != 0) { currentTable = seg.table; for (int j = currentTable.length - 1; j >= 0; --j) { if ( (nextEntry = currentTable[j]) != null) { nextTableIndex = j - 1; return; } } } } }不想再多介绍了，唯一需要注意的是跳到下一个段时，一定要先读取下一个段的 count 变量。这种迭代方式的主要效果是不会抛出 ConcurrentModificationException。一旦获取到下一个段的 table，也就意味着这个段的头结点在迭代过程中就确定了，在迭代过程中就不能反映对这个段节点并发的删除和添加，对于节点的更新是能够反映的，因为节点的值是一个 volatile 变量。结束语ConcurrentHashMap 是一个支持高并发的高性能的 HashMap 实现，它支持完全并发的读以及一定程度并发的写。ConcurrentHashMap 的实现也是很精巧，充分利用了最新的 JMM 规范，值得学习，却不值得模仿。最后由于本人水平有限，对大师的作品难免有误解，如果存在，还望大牛们不吝指出。装载自：http://www.iteye.com/topic/344876版本说明说明：本文内容基于 Java 7 及以下版本的 ConcurrentHashMap 实现（基于 Segment 分段锁机制）。Java 8 及之后版本已对该类进行了重构，改为使用 CAS + synchronized + 红黑树结构，不再使用 Segment 类，具体实现细节与本文所述有所不同。
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()); List l = Collections.synchronizedList(new ArrayList()); // put-if-absent idiom -- contains a race condition // may require external synchronization if (!map.containsKey(key)) map.put(key, value); // ad-hoc iteration -- contains race conditions // may require external synchronization for (int i=0; i<list.size(); i++) { doSomething(list.get(i)); } // normal iteration -- can throw ConcurrentModificationException // may require external synchronization for (Iterator i=list.iterator(); i.hasNext(); ) { doSomething(i.next()); }安全性的错觉synchronizedList 和 synchronizedMap 提供的有条件线程安全性也带来了一个隐患——开发者会假设，因为这些集合都是同步的，所以它们都是线程安全的，这样一来他们对于正确地同步复合操作这件事就会疏忽。其结果是尽管表面上这些程序在负载较轻的时候能够正常工作，但是一旦负载较重，它们就会开始抛出 NullPointerException 或 ConcurrentModificationException。可伸缩性瓶颈可伸缩性指的是一个应用程序在工作负载和可用处理资源增加时其吞吐量的表现情况。一个可伸缩的程序能够通过使用更多的处理器、内存或者 I/O 带宽来相应地处理更大的工作负载。锁住某个共享的资源以获得独占式的访问这种做法会形成可伸缩性瓶颈——它使其他线程不能访问那个资源，即使有空闲的处理器可以调用那些线程也无济于事。为了取得可伸缩性，我们必须消除或者减少我们对独占式资源锁的依赖。同步的集合包装器以及早期的 Hashtable 和 Vector 类带来的更大的问题是，它们在单个的锁上进行同步。这意味着一次只有一个线程可以访问集合，如果有一个线程正在读一个 Map，那么所有其他想要读或者写这个 Map 的线程就必须等待。最常见的 Map 操作，get() 和 put()，可能比表面上要进行更多的处理——当遍历一个 Hash 表的 bucket 以期找到某一特定的 key 时，get() 必须对大量的候选 bucket 调用 Object.equals()。如果 key 类所使用的 hashCode() 函数不能将 value 均匀地分布在整个 Hash 表范围内，或者存在大量的 Hash 冲突，那么某些 bucket 链就会比其他的链长很多，而遍历一个长的 Hash 链以及对该 Hash 链上一定百分比的元素调用 equals() 是一件很慢的事情。在上述条件下，调用 get() 和 put() 的代价高的问题不仅仅是指访问过程的缓慢，而且，当有线程正在遍历那个 Hash 链时，所有其他线程都被锁在外面，不能访问这个 Map。（哈希表根据一个叫做 Hash 的数字关键字（key）将对象存储在 bucket 中。Hash value 是从对象中的值计算得来的一个数字。每个不同的 Hash value 都会创建一个新的 bucket。要查找一个对象，您只需要计算这个对象的 Hash value 并搜索相应的 bucket 就行了。通过快速地找到相应的 bucket，就可以减少您需要搜索的对象数量了。译者注）get() 执行起来可能会占用大量的时间，而在某些情况下，前面已经作了讨论的有条件线程安全性问题会让这个问题变得还要糟糕得多。清单 1 中演示的争用条件常常使得对单个集合的锁在单个操作执行完毕之后还必须继续保持一段较长的时间。如果您要在整个迭代期间都保持对集合的锁，那么其他的线程就会在锁外停留很长的一段时间，等待解锁。实例：简单缓存Map 在服务器应用中最常见的应用之一就是实现一个 cache。服务器应用可能需要缓存文件内容、生成的页面、数据库查询的结果、与经过解析的 XML 文件相关的 DOM 树，以及许多其他类型的数据。Cache 的主要用途是重用前一次处理得出的结果以减少服务时间和增加吞吐量。Cache 工作负载的一个典型的特征就是检索大大多于更新，因此（理想情况下）Cache 能够提供非常好的 get() 性能。不过，使用会妨碍性能的 Cache 还不如完全不用 Cache。如果使用 synchronizedMap 来实现一个 Cache，那么您就在您的应用程序中引入了一个潜在的可伸缩性瓶颈。因为一次只有一个线程可以访问 Map，这些线程包括那些要从 Map 中取出一个值的线程以及那些要将一个新的 (key, value) 对插入到该 Map 中的线程。减小锁粒度提高 HashMap 的并发性同时还提供线程安全性的一种方法是废除对整个表使用一个锁的方式，而采用对 Hash 表的每个 bucket 都使用一个锁的方式（或者，更常见的是，使用一个锁池，每个锁负责保护几个 bucket）。这意味着多个线程可以同时地访问一个 Map 的不同部分，而不必争用单个的集合范围的锁。这种方法能够直接提高插入、检索以及移除操作的可伸缩性。不幸的是，这种并发性是以一定的代价换来的——这使得对整个集合进行操作的一些方法（例如 size() 或 isEmpty()）的实现更加困难，因为这些方法要求一次获得许多的锁，并且还存在返回不正确的结果的风险。然而，对于某些情况，例如实现 Cache，这样做是一个很好的折衷——因为检索和插入操作比较频繁，而 size() 和 isEmpty() 操作则少得多。ConcurrentHashMaputil.concurrent 包中的 ConcurrentHashMap 类（也将出现在 JDK 1.5 中的 java.util.concurrent 包中）是对 Map 的线程安全的实现，比起 synchronizedMap 来，它提供了好得多的并发性。多个读操作几乎总可以并发地执行，同时进行的读和写操作通常也能并发地执行，而同时进行的写操作仍然可以不时地并发进行（相关的类也提供了类似的多个读线程的并发性，但是，只允许有一个活动的写线程）。ConcurrentHashMap 被设计用来优化检索操作；实际上，成功的 get() 操作完成之后通常根本不会有锁着的资源。要在不使用锁的情况下取得线程安全性需要一定的技巧性，并且需要对 Java 内存模型（Java Memory Model）的细节有深入的理解。ConcurrentHashMap 实现，加上 util.concurrent 包的其他部分，已经被研究正确性和线程安全性的并发专家所正视。在下个月的文章中，我们将看看 ConcurrentHashMap 的实现的细节。ConcurrentHashMap 通过稍微地松弛它对调用者的承诺而获得了更高的并发性。检索操作将可以返回由最近完成的插入操作所插入的值，也可以返回在步调上是并发的插入操作所添加的值（但是决不会返回一个没有意义的结果）。由 ConcurrentHashMap.iterator() 返回的 Iterators 将每次最多返回一个元素，并且决不会抛出 ConcurrentModificationException 异常，但是可能会也可能不会反映在该迭代器被构建之后发生的插入操作或者移除操作。在对集合进行迭代时，不需要表范围的锁就能提供线程安全性。在任何不依赖于锁整个表来防止更新的应用程序中，可以使用 ConcurrentHashMap 来替代 synchronizedMap 或 Hashtable。上述改进使得 ConcurrentHashMap 能够提供比 Hashtable 高得多的可伸缩性，而且，对于很多类型的常见案例（比如共享的 Cache）来说，还不用损失其效率。性能提升有多少？表 1 对 Hashtable 和 ConcurrentHashMap 的可伸缩性进行了粗略的比较。在每次运行过程中，n 个线程并发地执行一个死循环，在这个死循环中这些线程从一个 Hashtable 或者 ConcurrentHashMap 中检索随机的 key value，发现在执行 put() 操作时有 80% 的检索失败率，在执行操作时有 1% 的检索成功率。测试所在的平台是一个双处理器的 Xeon 系统，操作系统是 Linux。数据显示了 10,000,000 次迭代以毫秒计的运行时间，这个数据是在将对 ConcurrentHashMap 的操作标准化为一个线程的情况下进行统计的。您可以看到，当线程增加到多个时，ConcurrentHashMap 的性能仍然保持上升趋势，而 Hashtable 的性能则随着争用锁的情况的出现而立即降了下来。比起通常情况下的服务器应用，这次测试中线程的数量看上去有点少。然而，因为每个线程都在不停地对表进行操作，所以这与实际环境下使用这个表的更多数量的线程的争用情况基本等同。表 1. Hashtable 与 ConcurrentHashMap 在可伸缩性方面的比较线程数ConcurrentHashMapHashtable11.001.0322.5932.4045.5878.23813.21163.481627.58341.213257.27778.41CopyOnWriteArrayList在那些遍历操作大大地多于插入或移除操作的并发应用程序中，一般用 CopyOnWriteArrayList 类替代 ArrayList。如果是用于存放一个侦听器（listener）列表，例如在 AWT 或 Swing 应用程序中，或者在常见的 JavaBean 中，那么这种情况很常见（相关的 CopyOnWriteArraySet 使用一个 CopyOnWriteArrayList 来实现 Set 接口）。如果您正在使用一个普通的 ArrayList 来存放一个侦听器列表，那么只要该列表是可变的，而且可能要被多个线程访问，您就必须要么在对其进行迭代操作期间，要么在迭代前进行的克隆操作期间，锁定整个列表，这两种做法的开销都很大。当对列表执行会引起列表发生变化的操作时，CopyOnWriteArrayList 并不是为列表创建一个全新的副本，它的迭代器肯定能够返回在迭代器被创建时列表的状态，而不会抛出 ConcurrentModificationException。在对列表进行迭代之前不必克隆列表或者在迭代期间锁定列表，因为迭代器所看到的列表的副本是不变的。换句话说，CopyOnWriteArrayList 含有对一个不可变数组的一个可变的引用，因此，只要保留好那个引用，您就可以获得不可变的线程安全性的好处，而且不用锁定列表。结束语同步的集合类 Hashtable 和 Vector，以及同步的包装器类 Collections.synchronizedMap 和 Collections.synchronizedList，为 Map 和 List 提供了基本的有条件线程安全的实现。然而，某些因素使得它们并不适用于具有高度并发性的应用程序中——它们的集合范围的单锁特性对于可伸缩性来说是一个障碍，而且，很多时候还必须在一段较长的时间内锁定一个集合，以防止出现 ConcurrentModificationException 异常。ConcurrentHashMap 和 CopyOnWriteArrayList 实现提供了更高的并发性，同时还保住了线程安全性，只不过在对其调用者的承诺上打了点折扣。ConcurrentHashMap 和 CopyOnWriteArrayList 并不是在您使用 HashMap 或 ArrayList 的任何地方都一定有用，但是它们是设计用来优化某些特定的常见解决方案的。许多并发应用程序将从对它们的使用中获得好处。相关主题您可以参阅本文在 developerWorks 全球站点上的 英文原文。参与有关本文的 论坛)（您也可以单击文章顶部或底部的 讨论来访问论坛）。阅读由 Brian Goetz 撰写的 完整的 Java 理论与实践 系列。特别地，在"变化，还是不变化？" (developerWorks, 2003 年 2 月) 一文中还讨论了不变性的线程安全性好处。Doug Lea 的 Concurrent Programming in Java, Second Edition 这本书对有关 Java 应用程序中的多线程编程中的一些微妙的问题作了精妙的论述。下载 util.concurrent 包。ConcurrentHashMap 的 Javadoc 页面更详细地解释了 ConcurrentHashMap 和 Hashtable 之间的不同之处，也可参考 相关讨论。JSR 166 是用于 JDK 1.5 的标准化的 util.concurrent 库。2002 年 11 月那一期的 Java 理论与实践 研究了 util.concurrent 的执行工具。"轻松线程化，第二部分" (developerWorks, 2001 年 9 月) 中探讨了争用所致的代价。在 developerWorks Java 技术专区，可以找到数百篇有关 Java 编程各方面的文章。说明：本文翻译自早期技术文章，文中提及的 JDK 1.5 现已发布并成为 Java 标准版本（Java 5）。java.util.concurrent 包已成为现代 Java 并发编程的核心库。文中关于性能对比的数据基于当时的硬件环境（双处理器 Xeon 系统），仅供参考历史背景与原理分析。
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) { Thread.sleep(1000); // LOCK 依然被当前线程持有 }wait() 示例在 synchronized 块中调用 wait()，锁会被释放：synchronized(LOCK) { LOCK.wait(); // LOCK 被释放，直到被唤醒并重新获取锁 }推荐阅读：yield() 和 join() 之间的区别3. 对比总结简而言之，我们将以上要点进行分类总结，以便记忆和理解。特性sleep()wait()调用对象调用 Thread 类（静态方法）；始终影响当前正在执行的线程。调用 Object 类；当前线程必须在锁对象上同步。同步要求不需要同步上下文，但在同步块中使用时会持有锁。必须在 synchronized 同步块或方法中调用，否则抛出 IllegalMonitorStateException。锁行为不释放锁。休眠期间，其他线程无法进入同一对象的同步块。释放锁。允许其他线程有机会执行同步代码。唤醒条件直到指定时间到期，或调用 interrupt() 中断。直到其他线程调用对象的 notify() 或 notifyAll()。主要用途用于时间同步、暂停执行或模拟延时。用于多线程间的协作与通信。希望以上信息能为您的知识库增加价值。学习愉快！
1. 乐观锁与悲观锁 (Optimistic and Pessimistic Locking)传统的锁定机制（例如 Java 中的 synchronized 关键字）被称为悲观锁（Pessimistic Locking）技术。它要求线程在访问共享资源前必须先获得锁，确保没有其他线程会干扰操作（即锁定对象），然后才允许访问实例或方法。这就好比说：“请先关上门，否则其他干扰者会进来重新整理您的东西。”尽管上述方法是安全的且确实有效，但它会给应用程序的性能带来显著损失。原因很简单：等待锁的线程无法执行任何操作，直到它们有机会获取锁并执行受保护的操作为止。存在一种在性能上更有效且本质上是乐观的方法。使用这种方法，线程可以直接进行更新，假设在操作期间不会发生干扰。此方法依靠冲突检测来确定更新期间是否存在来自其他线程的干扰；在这种情况下，操作将失败，线程可以选择重试（或不重试）。乐观锁的方法就像那句老话所说：“请求原谅比获得许可更容易（Easier to ask forgiveness than permission）”，这里的“容易”意味着“更高效”。比较并交换（Compare-And-Swap, CAS） 就是这种乐观方法的一个典型例子，我们将在下面详细讨论。2. 比较并交换算法 (Compare and Swap Algorithm)CAS 算法将存储位置的内容与给定值进行比较，并且只有当它们相同时，才将该存储位置的内容修改为给定的新值。这是作为单个原子操作（Atomic Operation）完成的。原子性保证了新值是根据最新信息计算的；如果与此同时值已被另一个线程更新，则写入将失败。操作结果必须表明是否执行了替换；这可以通过简单的布尔响应（此变量通常称为“比较设置”）来完成，也可以通过返回从内存位置读取的值（而不是写入该值）来完成。CAS 操作包含三个操作数：内存位置 V：必须替换值的存储位置。预期原值 A：线程上次读取的旧值。新值 B：应该写在 V 上的新值。CAS 的逻辑可以表述为：“我认为 V 应该具有值 A；如果可以，则将 B 放在此处；否则不要更改它，但要告诉我操作失败了。”CAS 是一种乐观技术，它希望成功进行更新，并且如果自从上次检查变量以来另一个线程更新了该变量，它可以检测到失败。3. Java CAS 示例 (Java Compare and Swap Example)让我们通过一个例子来了解整个过程。假设 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) { V = B; // 更新成功 } else { // 操作失败 return V; // 返回当前值 }显然，V 的值等于 A，因此 V 将被覆盖为 11，操作成功。3) 线程 2 执行 CAS线程 2 随后到来，尝试执行与线程 1 相同的操作：状态：V = 11 (已被线程 1 修改), A = 10 (线程 2 旧的预期值), B = 11if (A == V) { V = B; } else { // 操作失败 return V; }4) 线程 2 重试在这种情况下，V (11) 不等于 A (10)，因此不替换值，操作失败。线程 2 获取到当前最新值 11，再次使用新值重试此操作：新状态：V = 11, A = 11, B = 12这一次，条件得到满足，增量值 12 被成功写入，操作返回成功。总结总而言之，当多个线程尝试使用 CAS 同时更新同一变量时，只有一个线程会获胜并更新该变量的值，其余线程会发现操作失败。但是，失败者并不会因为线程阻塞而受到惩罚。它们可以自由地重试该操作（自旋），或者根据业务逻辑放弃操作。这就是 Java 中原子操作支持背后的简单但重要的概念。说明：本文基于 Java 5 引入的 java.util.concurrent.atomic 包机制编写。CAS 算法是现代 Java 并发编程（如 AtomicInteger、AtomicLong 等类）的底层基础，适用于高并发场景下的无锁编程。
Java 并发性 – 线程安全性？定义线程安全性（Thread Safety）出奇地棘手。快速搜索会发现许多类似的“定义”：线程安全代码是即使许多线程同时执行也可以运行的代码。如果一段代码仅以保证多个线程同时安全执行的方式操作共享数据结构，则它是线程安全的。此外还有更多类似的定义。你是否觉得上述定义实际上并未传达任何有意义的信息，甚至增加了一些混乱？尽管不能排除这些定义，因为它们没有错，但事实是它们没有提供任何实际帮助或观点。我们如何区分线程安全的类和不安全的类？我们甚至不清楚“安全”究竟意味着什么。什么是线程安全的正确性？线程安全性任何合理定义的核心是正确性（Correctness）的概念。因此，在了解线程安全性之前，我们应该首先了解“正确性”。正确性表示一个类符合其规范（Specification）。你将同意，良好的类规范将在任何给定时间包含有关类状态的所有信息，并且如果对它执行了某些操作，则它是后置条件（Postcondition）。由于我们经常没有为类编写足够的规范，怎么可能知道它们是正确的？我们不能。但是，一旦我们说服自己“代码可以工作”，那也不会阻止我们继续使用它们。这种“代码信心”与我们许多人接近正确性的程度差不多。乐观地将“正确性”定义为可以识别的事物之后，我们现在可以以一种非循环的方式定义线程安全性：当一个类在从多个线程访问时继续正确运行时，它是线程安全的。如果一个类在从多个线程访问时能正确运行，则无论运行时环境如何调度或交织那些线程的执行，并且在调用代码部分没有任何其他同步或其他协调的情况下，如果它行为正确，则该线程是安全的。如果此处对“正确性”的宽松使用使您感到困扰，则您可能更愿意将线程安全类认为是在并发环境中比在单线程环境中不会损坏的类。线程安全类封装了任何需要的同步，因此客户端不需要提供自己的同步。示例：无状态 Servlet线程安全类的一个很好的例子是 Java Servlet，它没有字段和引用，也没有其他类的字段等。它们是无状态的（Stateless）。public class StatelessFactorizer implements Servlet { public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); encodeIntoResponse(resp, factors); } }特定计算的中间状态仅存在于局部变量中，这些局部变量存储在线程的栈（Stack）中，并且只有执行线程才能访问。一个线程访问 StatelessFactorizer 不会影响另一个线程访问 StatelessFactorizer 的结果；因为两个线程不共享状态，所以好像它们正在访问不同的实例。由于线程访问无状态对象的动作不会影响其他线程中操作的正确性，因此无状态对象是线程安全的。这就是关于什么是线程安全的小而重要的概念的全部内容。学习愉快！
HashMap vs ConcurrentHashMap — 示例及Iterator探秘
引言作为一名 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) { // ConcurrentHashMap Map<String, String> myMap = new ConcurrentHashMap<String, String>(); myMap.put("1", "1"); myMap.put("2", "1"); myMap.put("3", "1"); myMap.put("4", "1"); myMap.put("5", "1"); myMap.put("6", "1"); System.out.println("ConcurrentHashMap before iterator: " + myMap); Iterator<String> it = myMap.keySet().iterator(); while (it.hasNext()) { String key = it.next(); if (key.equals("3")) myMap.put(key + "new", "new3"); } System.out.println("ConcurrentHashMap after iterator: " + myMap); // HashMap myMap = new HashMap<String, String>(); myMap.put("1", "1"); myMap.put("2", "1"); myMap.put("3", "1"); myMap.put("4", "1"); myMap.put("5", "1"); myMap.put("6", "1"); System.out.println("HashMap before iterator: " + myMap); Iterator<String> it1 = myMap.keySet().iterator(); while (it1.hasNext()) { String key = it1.next(); if (key.equals("3")) myMap.put(key + "new", "new3"); } System.out.println("HashMap after iterator: " + myMap); } }运行结果与分析当我们试着运行上面的程序，输出如下：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) at java.util.HashMap$KeyIterator.next(HashMap.java:828) at com.journaldev.util.ConcurrentHashMapExample.main(ConcurrentHashMapExample.java:44)查看输出结果，很明显 ConcurrentHashMap 可以支持向 Map 中添加新元素，而 HashMap 则抛出了 ConcurrentModificationException。查看异常堆栈记录，可以发现是下面这条语句抛出了异常：String key = it1.next();这就意味着新的元素在 HashMap 中已经插入了，但是在迭代器执行 next() 时出现了错误。事实上，集合对象的迭代器提供了快速失败（Fail-Fast）机制，即修改集合对象结构或者元素数量都会使迭代器触发这个异常。深入原理：modCount那么，迭代器是怎么知道 HashMap 被修改了呢？我们可以尝试一次性取出 HashMap 的所有 Key 然后进行遍历。HashMap 包含一个修改计数器，当你调用它的 next() 方法来获取下一个元素时，迭代器将会用到这个计数器。HashMap.java/** * HashMap 结构的修改次数 * 结构修改是指：改变了 HashMap 中 mapping 的个数或者其中的内部结构（比如，重新计算 hash 值） * 这个字段在通过 Collection 操作 Hashmap 时提供快速失败（Fail-fast）功能。 * （参见 ConcurrentModificationException）。 */ transient volatile int modCount;特殊情况：提前跳出循环现在为了证明上面的观点，我们对原来的代码做一点修改，使迭代器在插入新的元素后跳出循环。只要在调用 put 方法后增加一个 break：if (key.equals("3")) { myMap.put(key + "new", "new3"); break; }再执行修改后的代码，会得到下面的输出结果：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} HashMap after iterator: {3=1, 2=1, 1=1, 3new=new3, 6=1, 5=1, 4=1}此时 HashMap 不再抛出异常，因为循环在修改结构后立即终止，迭代器没有机会再次检查 modCount 的变化。思考：修改值而非结构最后，如果我们不添加新的元素，而是修改已经存在的键值对，会不会抛出异常呢？修改原来的程序并且自己验证一下：// myMap.put(key+"new", "new3"); myMap.put(key, "new3");如果你对于输出结果感觉困惑或者震惊，欢迎在评论区留言。我会很乐意给出进一步解释。关于泛型你有没有注意到我们在创建集合和迭代器时的尖括号？在 Java 中这叫做泛型（Generics），当涉及到编译时的类型检查和去除运行时的 ClassCastException 的时候会很有帮助。点击 这里 可以了解更多泛型教程。原文链接：journaldev说明：本文示例代码采用了显式声明泛型类型的写法（如 new HashMap<String, String>()），适用于 Java 7 之前版本。Java 7 及后续版本支持泛型类型推断（钻石操作符 <>），可简化代码书写，但不影响本文所述的核心原理。
Java Multi-threading Evolution and Topics
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 并发执行框架的优势之一是支持运行并发任务，并在任务处理后返回单个结果。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) { this.number = number; } @Override public Integer call() throws Exception { int result = 1; if ((number == 0) || (number == 1)) { result = 1; } else { for (int i = 2; i <= number; i++) { result *= i; TimeUnit.MILLISECONDS.sleep(20); } } System.out.println("Result for number - " + number + " -> " + result); 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) { ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2); List<Future<Integer>> resultList = new ArrayList<>(); Random random = new Random(); for (int i = 0; i < 4; i++) { Integer number = random.nextInt(10); FactorialCalculator calculator = new FactorialCalculator(number); Future<Integer> result = executor.submit(calculator); resultList.add(result); } for (Future<Integer> future : resultList) { try { System.out.println("Future result is - " + " - " + future.get() + "; And Task done is " + future.isDone()); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } // shut down the executor service now executor.shutdown(); } }程序输出示例：Result for number - 4 -> 24 Result for number - 6 -> 720 Future result is - - 720; And Task done is true Future result is - - 24; And Task done is true Result for number - 2 -> 2 Result for number - 6 -> 720 Future result is - - 720; And Task done is true Future result is - - 2; And Task done is true在此示例中，我们使用 submit() 方法将 Callable 对象提交给执行器（Executor）执行。该方法接收一个 Callable 对象作为参数，并返回一个 Future 对象。Future 对象主要用于以下两个目的：控制任务状态：我们可以取消任务或检查任务是否完成。为此，我们使用 isDone() 方法来检查任务状态。获取返回结果：我们可以通过 call() 方法获得返回的结果。为此，我们使用 get() 方法。该方法会阻塞直到 Callable 对象完成 call() 方法的执行并返回结果。如果线程在 get() 方法等待结果时被中断，它将抛出 InterruptedException 异常。如果 call() 方法执行过程中抛出异常，则 get() 方法会抛出 ExecutionException 异常。Future 接口还提供了另一个 get() 方法的重载版本：get(long timeout, TimeUnit unit)。如果在指定时间内结果仍不可用，该方法将抛出 TimeoutException 异常，而不是无限期等待。参考：http://images.jsdiff.com/archives/15507075说明：本文示例基于 Java 5 及以上版本，Callable 与 Future 核心接口行为在后续版本中保持稳定。