明明用了ConcurrentHashMap,可是始终线程不安全,
下面我们来看代码:
1 public class Test40 { 2 3 public static void main(String[] args) throws InterruptedException { 4 for (int i = 0; i < 10; i++) { 5 System.out.println(test()); 6 } 7 } 8 9 private static int test() throws InterruptedException { 10 ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>(); 11 ExecutorService pool = Executors.newCachedThreadPool(); 12 for (int i = 0; i < 8; i++) { 13 pool.execute(new MyTask(map)); 14 } 15 pool.shutdown(); 16 pool.awaitTermination(1, TimeUnit.DAYS); 17 18 return map.get(MyTask.KEY); 19 } 20 } 21 22 class MyTask implements Runnable { 23 24 public static final String KEY = "key"; 25 26 private ConcurrentHashMap<String, Integer> map; 27 28 public MyTask(ConcurrentHashMap<String, Integer> map) { 29 this.map = map; 30 } 31 32 @Override 33 public void run() { 34 for (int i = 0; i < 100; i++) { 35 this.addup(); 36 } 37 } 38 39 private void addup() { 40 if (!map.containsKey(KEY)) { 41 map.put(KEY, 1); 42 } else { 43 map.put(KEY, map.get(KEY) + 1); 44 } 45 } 46 }
测试代码跑了10次,每次都不是800。这就很让人疑惑了,难道ConcurrentHashMap的线程安全性失效了?
查了一些资料后发现,原来ConcurrentHashMap的线程安全指的是,它的每个方法单独调用(即原子操作)都是线程安全的,但是代码总体的互斥性并不受控制。以上面的代码为例,最后一行中的:
1 map.put(KEY, map.get(KEY) + 1);
实际上并不是原子操作,它包含了三步:
- map.get
- 加1
- map.put
其中第1和第3步,单独来说都是线程安全的,由ConcurrentHashMap保证。但是由于在上面的代码中,map本身是一个共享变量。当线程A执行map.get的时候,其它线程可能正在执行map.put,这样一来当线程A执行到map.put的时候,线程A的值就已经是脏数据了,然后脏数据覆盖了真值,导致线程不安全
简单地说,ConcurrentHashMap的get方法获取到的是此时的真值,但它并不保证当你调用put方法的时候,当时获取到的值仍然是真值
为了使上面的代码变得线程安全,我引入了synchronized关键字来修饰目标方法,如下:
1 public class Test40 { 2 3 public static void main(String[] args) throws InterruptedException { 4 for (int i = 0; i < 10; i++) { 5 System.out.println(test()); 6 } 7 } 8 9 private static int test() throws InterruptedException { 10 ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>(); 11 ExecutorService pool = Executors.newCachedThreadPool(); 12 for (int i = 0; i < 8; i++) { 13 pool.execute(new MyTask(map)); 14 } 15 pool.shutdown(); 16 pool.awaitTermination(1, TimeUnit.DAYS); 17 18 return map.get(MyTask.KEY); 19 } 20 } 21 22 class MyTask implements Runnable { 23 24 public static final String KEY = "key"; 25 26 private ConcurrentHashMap<String, Integer> map; 27 28 public MyTask(ConcurrentHashMap<String, Integer> map) { 29 this.map = map; 30 } 31 32 @Override 33 public void run() { 34 for (int i = 0; i < 100; i++) { 35 this.addup(); 36 } 37 } 38 39 private synchronized void addup() { // 用关键字synchronized修饰addup方法 40 if (!map.containsKey(KEY)) { 41 map.put(KEY, 1); 42 } else { 43 map.put(KEY, map.get(KEY) + 1); 44 } 45 } 46 47 }
运行之后仍然是线程不安全的,难道synchronized也失效了?
查阅了synchronized的资料后,原来,不管synchronized是用来修饰方法,还是修饰代码块,其本质都是锁定某一个对象。修饰方法时,锁上的是调用这个方法的对象,即this;修饰代码块时,锁上的是括号里的那个对象
在上面的代码中,很明显就是锁定的MyTask对象本身。但是由于在每一个线程中,MyTask对象都是独立的,这就导致实际上每个线程都对自己的MyTask进行锁定,而并不会干涉其它线程的MyTask对象。换言之,上锁压根没有意义
理解到这点之后,对上面的代码又做了一次修改:
1 public class Test40 { 2 3 public static void main(String[] args) throws InterruptedException { 4 for (int i = 0; i < 10; i++) { 5 System.out.println(test()); 6 } 7 } 8 9 private static int test() throws InterruptedException { 10 ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>(); 11 ExecutorService pool = Executors.newCachedThreadPool(); 12 for (int i = 0; i < 8; i++) { 13 pool.execute(new MyTask(map)); 14 } 15 pool.shutdown(); 16 pool.awaitTermination(1, TimeUnit.DAYS); 17 18 return map.get(MyTask.KEY); 19 } 20 } 21 22 class MyTask implements Runnable { 23 24 public static final String KEY = "key"; 25 26 private ConcurrentHashMap<String, Integer> map; 27 28 public MyTask(ConcurrentHashMap<String, Integer> map) { 29 this.map = map; 30 } 31 32 @Override 33 public void run() { 34 for (int i = 0; i < 100; i++) { 35 synchronized (map) { // 对共享对象map上锁 36 this.addup(); 37 } 38 } 39 } 40 41 private void addup() { 42 if (!map.containsKey(KEY)) { 43 map.put(KEY, 1); 44 } else { 45 map.put(KEY, map.get(KEY) + 1); 46 } 47 } 48 49 }
此时在调用addup时直接锁定map,由于map是被所有线程共享的,因而达到了让所有线程互斥的目的,线程安全达成。
修改后,ConcurrentHashMap的作用就不大了,可以直接将代码中的map换成普通的HashMap,以减少由ConcurrentHashMap带来的锁开销
最后特别补充的是,synchronized关键字判断对象是否是它属于锁定的对象,本质上是通过 == 运算符来判断的。换句话说,上面的代码中,可以采用任何一个常量,或者每个线程都共享的变量,或者MyTask类的静态变量,来代替map。只要该变量与synchronized锁定的目标变量相同(==),就可以使synchronized生效
综上,代码最终可以修改为:
1 public class Test40 { 2 3 public static void main(String[] args) throws InterruptedException { 4 for (int i = 0; i < 100; i++) { 5 System.out.println(test()); 6 } 7 } 8 9 private static int test() throws InterruptedException { 10 Map<String, Integer> map = new HashMap<String, Integer>(); 11 ExecutorService pool = Executors.newCachedThreadPool(); 12 for (int i = 0; i < 8; i++) { 13 pool.execute(new MyTask(map)); 14 } 15 pool.shutdown(); 16 pool.awaitTermination(1, TimeUnit.DAYS); 17 18 return map.get(MyTask.KEY); 19 } 20 } 21 22 class MyTask implements Runnable { 23 24 public static Object lock = new Object(); 25 26 public static final String KEY = "key"; 27 28 private Map<String, Integer> map; 29 30 public MyTask(Map<String, Integer> map) { 31 this.map = map; 32 } 33 34 @Override 35 public void run() { 36 for (int i = 0; i < 100; i++) { 37 synchronized (lock) { 38 this.addup(); 39 } 40 } 41 } 42 43 private void addup() { 44 if (!map.containsKey(KEY)) { 45 map.put(KEY, 1); 46 } else { 47 map.put(KEY, map.get(KEY) + 1); 48 } 49 } 50 51 }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构