为什么多线程操作HashMap会导致书本神秘失踪?

图片

序章:欢迎来到万物图书馆

想象一下,我们正站在一座宏伟而古老的“万物图书馆”门前。这座图书馆并非凡间之物,它收藏着宇宙间所有的知识,每一份知识都以“书名”(Key)和“内容”(Value)的形式,被精心存放在书架上。我们的任务,就是要了解图书馆里两种不同的图书管理系统是如何运作的。


第一卷:HashMap - 单一图书管理员的小书斋

1.1 书斋的诞生

// 代码片段 1: HashMap 的基本情况
Map<String, Integer> wordCounts = new HashMap<>();

故事解读:
在图书馆的一隅,有一间名为 wordCounts 的传统小书斋。它由 HashMap 大师设计,非常适合一位名叫“单线程先生”的图书管理员独自工作。他在这里可以飞快地找到书(get)、放入新书(put),或者偶尔整理一下书架(内部操作如 resize)。因为只有他一人,书斋的门上无需加锁,一切都显得那么宁静高效。

1.2 不速之客:当书斋挤满管理员

// 代码片段 2: HashMap 在多线程下的隐患 (模拟)
ExecutorServiceexecutor= Executors.newFixedThreadPool(10);
intoperations=1000;

for (inti=0; i < operations; i++) {
    executor.submit(() -> {
        // This is NOT a safe operation for HashMap with multiple threads
        wordCounts.put("word" + (int)(Math.random() * 100), 1);
    });
}
// ...
System.out.println("HashMap size after concurrent puts (potentially problematic): " + wordCounts.size());

故事解读:
突然有一天,图书馆接到了紧急通知,需要同时处理大量的书籍!于是,10位图书管理员(Executors.newFixedThreadPool(10))被派到了这个原本只为“单线程先生”设计的小书斋。他们都想同时往书斋里放新书(wordCounts.put(...))。

这下可乱了套!

  • • 书本的神秘失踪与书架的混乱(Lost Updates & Internal Corruption): 想象一下,两位管理员同时看中了书架上的同一个空位,都想把自己的书放上去。结果,一个人的书刚放稳,另一个人的书紧随其后,把前一本给挤掉了!或者,他们争抢之下,谁的书都没放稳,最后书掉到了地上,无人知晓。这就是为什么最终书斋里的书的数量(wordCounts.size())可能和预期不符。更糟糕的是,如果一位管理员正在调整书架的隔板(resize操作,因为书太多需要更大的空间),其他管理员还在拼命塞书,整个书架的结构就可能被他们弄得一团糟,甚至可能进入一种“怎么也整理不好”的怪圈(无限循环)。

  • • 管理员的惊呼(ConcurrentModificationException): 如果“单线程先生”正在仔细清点书斋里的书目(迭代),其他冒失的管理员突然闯进来抽走一本书,或者塞进一本新书,“单线程先生”会立刻发现账目对不上,大声惊呼:“在我眼皮底下,书架变了!”

1.3 书斋的随性规定

// 代码片段 3: HashMap 对 null 的态度
wordCounts.put(null, 0);
wordCounts.put("testNullValue", null);
System.out.println("HashMap allows null key: " + wordCounts.containsKey(null));
System.out.println("HashMap allows null value for 'testNullValue': " + (wordCounts.get("testNullValue") == null));

故事解读:
“单线程先生”管理的小书斋,在规定上比较随和。他允许你放一本“没有书名”(null key)的特殊书籍,也允许某些书籍的内容是“一片空白”(null value)。毕竟只有他一个人打理,他能记住这些特殊书籍放在哪里,以及它们的特殊性。

1.4 小书斋的智慧箴言

HashMap 小书斋,就像一个高效但脆弱的单人工作室。在一位管理员独自工作时,它的效率无人能及。但若是多位管理员不加协调地同时闯入,便会引发一场大混乱。


第二卷:ConcurrentHashMap - 秩序井然的魔法大书库

2.1 魔法大书库的宏伟亮相

// 代码片段 4: ConcurrentHashMap 的登场
Map<String, Integer> concurrentWordCounts = new ConcurrentHashMap<>();

故事解读:
为了应对图书馆日益增长的繁忙业务,一座名为 concurrentWordCounts 的“魔法大书库”被建立起来。它由 ConcurrentHashMap 大师运用精妙的并发魔法构建,从设计之初就考虑到了大量图书管理员(线程)同时工作的场景。

2.2 魔法书库的协作奥秘

// 代码片段 5: ConcurrentHashMap 在多线程下的表现
ExecutorServiceexecutor= Executors.newFixedThreadPool(10);
intoperations=1000;

for (inti=0; i < operations; i++) {
    executor.submit(() -> {
        Stringkey="word" + (int)(Math.random() * 100);
        // compute is an atomic operation, good for counters
        concurrentWordCounts.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
    });
}
// ...
System.out.println("ConcurrentHashMap size after concurrent puts (thread-safe): " + concurrentWordCounts.size());

故事解读:
当那10位图书管理员(Executors.newFixedThreadPool(10))被引导至这座魔法大书库时,一切都显得井然有序,即使他们同时投入工作:

2.2.1 分区管理的智慧:互不干扰的管理员们 (Segmentation / Node Locking)

魔法大书库的内部被巧妙地划分成许多独立的“区域”(Segments,在早期版本中)或者每个书架单元(Node,在较新版本中)都配备了独立的微型魔法锁。当一位管理员在某个区域或书架单元操作时,只有那一小块地方会被暂时锁定。其他管理员可以自由地在书库的其他区域或书架单元工作,互不干扰。这就像每位管理员都有自己负责的一小排书架,他们可以同时整理各自的书籍,效率大大提升。

2.2.2 原子魔法:瞬间完成的图书操作 (Atomic Operations)

某些操作,比如为一本书的“阅读次数”计数(concurrentWordCounts.compute(key, (k, v) -> (v == null) ? 1 : v + 1)),被施加了强大的“原子魔法”。这意味着这个操作要么不执行,要么就会在瞬间完成,绝不会被中途打断。即使多位管理员想同时更新同一本书的阅读次数,他们也会像绅士一样排队,等前一位的魔法咒语完成后再施法,从而确保了计数的绝对准确。

2.2.3 安全的巡视:总馆长的安心点兵 (Weakly Consistent Iterators)

如果总馆长想要巡视并清点魔法大书库的书目(迭代),他可以安心地进行。即使此时其他管理员正在忙碌地放入新书或取走旧书,总馆长的清点工作也不会被打断(不会有 ConcurrentModificationException),他看到的书目会是他开始清点那一刻书库的“快照”,或者能反映出在他清点过程中已经完成的、确定的改动,但绝不会看到一个正在变化中、混乱不堪的书架。

2.3 魔法书库的铁律

// 代码片段 6: ConcurrentHashMap 对 null 的严格规定
try {
    concurrentWordCounts.put(null, 1);
} catch (NullPointerException e) {
    System.out.println("ConcurrentHashMap correctly threw NullPointerException for null key.");
}
try {
    concurrentWordCounts.put("testNullValue", null);
} catch (NullPointerException e) {
    System.out.println("ConcurrentHashMap correctly threw NullPointerException for null value.");
}

故事解读:
这座魔法大书库有着非常严格的规定:它决不允许存放“没有书名”(null key)的书籍,也不允许任何书籍的“内容是一片空白”(null value)。为什么呢?因为在一个需要多人高效协作的环境里,“没有名字”或“没有内容”的书籍极易引起混淆和歧义。如果一位管理员说:“我去取那本没有名字的书”,其他管理员可能会面面相觑,不知所措。因此,为了保证清晰、明确和操作的安全性,魔法书库直接禁止了这类情况,一旦有人尝试违反规定,书库的警报系统(NullPointerException)就会立刻鸣响。

2.4 魔法书库的智慧箴言

ConcurrentHashMap 魔法大书库,是一个专为并发而生的“多人协作工作室”。它通过精巧的内部魔法(如分段锁或更细粒度的节点锁),允许多位管理员同时、高效且安全地工作。但它对可能引起歧义的“不明确”事物(如 null)有着更为严格的要求。


第三卷:图书馆的其他守护者

// 代码片段 7: Hashtable 和 Collections.synchronizedMap (概念提及)
// Hashtable: Legacy, thread-safe (synchronizes every method) but generally slower...
// Collections.synchronizedMap: A wrapper that makes a given Map thread-safe...
System.out.println("Hashtable and Collections.synchronizedMap are also thread-safe, but ConcurrentHashMap usually offers better performance...");

故事解读:
在我们的万物图书馆中,除了小书斋和魔法大书库,还存在着一些其他的图书管理方式,它们就像是图书馆里不同类型的守护者:

3.1 Hashtable - 老派大管家

这是一位资格非常老的图书大管家。他非常注重秩序,规定在任何时候,整个图书馆(所有方法都 synchronized)只能有一位管理员进入操作。虽然这样绝对安全,但如果想进图书馆的管理员一多,大家就只能在门口排起长长的队伍,等待进入,效率自然就低了。这位老管家也非常严格,同样不接受“没有书名”或“内容空白”的书籍。

3.2 Collections.synchronizedMap(new HashMap<>()) - 加装了警报系统的小书斋

这就像是我们将之前那个“单线程先生”的小书斋,在外面整体加装了一套强大的魔法警报与门禁系统。任何时候,这套系统只允许一位管理员通过门禁进入书斋内部操作。虽然安全得到了保障,但书斋内部的运作方式并没有改变。当多位管理员想要进入时,他们依然需要在门禁外排队,一个接一个地进入。因此,虽然安全,但并发性能的瓶颈依然存在于“一次只能进一人”的规则上。至于它是否允许“没有书名”或“空白内容”的书,则完全取决于它所守护的那个原始书斋(比如 HashMap)的规定。


终章:如何选择你的知识殿堂?

特性对比:小书斋 vs 魔法大书库

现在,冒险者,你已经了解了万物图书馆中几种不同的图书管理系统,让我们再次回顾它们的关键区别:

特性

HashMap (小书斋)

ConcurrentHashMap (魔法大书库)

管理员协作模式

单人模式 (非线程安全)

多人协作模式 (线程安全)

内部安保系统

无 (依赖外部秩序)

精细化分区/单元锁 (内部并发控制)

工作效率

单人工作时极速

多人同时工作时通常更优,单人工作时略逊于小书斋

"无名之书" (Null 键)

允许 (一本)

严禁 (引发警报 NullPointerException)

"空白之书" (Null 值)

允许 (多本)

严禁 (引发警报 NullPointerException)

馆长巡视规则

发现即报错 (Fail-fast 迭代器)

稳定快照 (Weakly consistent 迭代器)

何时选择?

  • • 当你确信你的“书斋”在任何时候都只会被一位“管理员”(单线程)访问时,或者你能亲自在书斋门口设立完美的秩序(例如,使用外部的 synchronized 锁住整个书斋的入口),那么选择HashMap小书斋会让你体验到极致的速度。

  • • 当你的“书库”需要多位“管理员”(多线程)同时进出、存取和修改书籍,并且你追求的是高吞吐量和流畅的并发体验时,那么毫无疑问,ConcurrentHashMap魔法大书库是你的不二之选。它是现代高并发魔法世界中的明星。

希望这个关于“万物图书馆”的故事,能让你对 HashMap 和 ConcurrentHashMap 的奥秘了然于胸!

完整代码如下:

import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

publicclassMapComparison {

    publicstaticvoidmain(String[] args)throws InterruptedException {
        System.out.println("--- HashMap Demo (Not Thread-Safe) ---");
        demonstrateHashMap();

        System.out.println("\n--- ConcurrentHashMap Demo (Thread-Safe) ---");
        demonstrateConcurrentHashMap();

        // Bonus: Briefly mention Hashtable and SynchronizedMap
        System.out.println("\n--- Other Thread-Safe Alternatives (Brief) ---");
        brieflyMentionOthers();
    }

    publicstaticvoiddemonstrateHashMap()throws InterruptedException {
        Map<String, Integer> wordCounts = newHashMap<>();

        // Simulating multiple threads trying to update the HashMap (problematic)
        ExecutorServiceexecutor= Executors.newFixedThreadPool(10);
        intoperations=1000;

        for (inti=0; i < operations; i++) {
            executor.submit(() -> {
                // This is NOT a safe operation for HashMap with multiple threads
                // It might lead to lost updates or even infinite loops during resize
                // For simplicity, we'll just do puts.
                // A more complex get-then-put would more easily show issues.
                wordCounts.put("word" + (int)(Math.random() * 100), 1);
            });
        }

        executor.shutdown();
        executor.awaitTermination(5, TimeUnit.SECONDS);

        // The size might not be 'operations' due to overwrites or lost updates.
        // In a real high-concurrency scenario, this could also throw ConcurrentModificationException
        // if another thread was iterating while these puts were happening, or internal structures get corrupted.
        System.out.println("HashMap size after concurrent puts (potentially problematic): " + wordCounts.size());
        // Example of allowing null key and values
        wordCounts.put(null, 0);
        wordCounts.put("testNullValue", null);
        System.out.println("HashMap allows null key: " + wordCounts.containsKey(null));
        System.out.println("HashMap allows null value for 'testNullValue': " + (wordCounts.get("testNullValue") == null));
    }

    publicstaticvoiddemonstrateConcurrentHashMap()throws InterruptedException {
        Map<String, Integer> concurrentWordCounts = newConcurrentHashMap<>();

        ExecutorServiceexecutor= Executors.newFixedThreadPool(10);
        intoperations=1000;

        for (inti=0; i < operations; i++) {
            executor.submit(() -> {
                // This is a safe operation for ConcurrentHashMap
                Stringkey="word" + (int)(Math.random() * 100);
                // compute is an atomic operation, good for counters
                concurrentWordCounts.compute(key, (k, v) -> (v == null) ? 1 : v + 1);
            });
        }

        executor.shutdown();
        executor.awaitTermination(5, TimeUnit.SECONDS);

        System.out.println("ConcurrentHashMap size after concurrent puts (thread-safe): " + concurrentWordCounts.size());
        // Trying to put null key or value will result in NullPointerException
        try {
            concurrentWordCounts.put(null, 1);
        } catch (NullPointerException e) {
            System.out.println("ConcurrentHashMap correctly threw NullPointerException for null key.");
        }
        try {
            concurrentWordCounts.put("testNullValue", null);
        } catch (NullPointerException e) {
            System.out.println("ConcurrentHashMap correctly threw NullPointerException for null value.");
        }
    }

    publicstaticvoidbrieflyMentionOthers() {
        // Hashtable: Legacy, thread-safe (synchronizes every method) but generally slower than ConcurrentHashMap.
        Map<String, String> hashtable = newHashtable<>();
        hashtable.put("key", "value");
        // System.out.println("Hashtable entry: " + hashtable.get("key"));
        // try { hashtable.put(null, "value"); } catch (NullPointerException e) { System.out.println("Hashtable: NPE for null key"); }
        // try { hashtable.put("key", null); } catch (NullPointerException e) { System.out.println("Hashtable: NPE for null value"); }


        // Collections.synchronizedMap: A wrapper that makes a given Map thread-safe by synchronizing every access.
        Map<String, String> synchronizedMap = Collections.synchronizedMap(newHashMap<>());
        synchronizedMap.put("key", "value");
        // System.out.println("SynchronizedMap entry: " + synchronizedMap.get("key"));
        // synchronizedMap.put(null, null); // Behavior depends on the wrapped map (HashMap allows nulls)
        // System.out.println("SynchronizedMap allows nulls if underlying HashMap does: " + synchronizedMap.containsKey(null));

        System.out.println("Hashtable and Collections.synchronizedMap are also thread-safe, but ConcurrentHashMap usually offers better performance in high-concurrency scenarios due to its more advanced locking mechanisms.");
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java干货

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值