ConcurrentHashMap、synchronized与线程安全

2023-05-16

最近做的项目中遇到一个问题:明明用了ConcurrentHashMap,可是始终线程不安全

除去项目中的业务逻辑,简化后的代码如下:

public class Test40 {

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            System.out.println(test());
        }
    }
    
    private static int test() throws InterruptedException {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 8; i++) {
            pool.execute(new MyTask(map));
        }
        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.DAYS);
        
        return map.get(MyTask.KEY);
    }
}

class MyTask implements Runnable {
    
    public static final String KEY = "key";
    
    private ConcurrentHashMap<String, Integer> map;
    
    public MyTask(ConcurrentHashMap<String, Integer> map) {
        this.map = map;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            this.addup();
        }
    }
    
    private void addup() {
        if (!map.containsKey(KEY)) {
            map.put(KEY, 1);
        } else {
            map.put(KEY, map.get(KEY) + 1);
        }    
    }
}

测试代码跑了10次,每次都不是800。这就很让人疑惑了,难道ConcurrentHashMap的线程安全性失效了?

查了一些资料后发现,原来ConcurrentHashMap的线程安全指的是,它的每个方法单独调用(即原子操作)都是线程安全的,但是代码总体的互斥性并不受控制。以上面的代码为例,最后一行中的:

map.put(KEY, map.get(KEY) + 1);

实际上并不是原子操作,它包含了三步:

  1. map.get
  2. 加1
  3. map.put

其中第1和第3步,单独来说都是线程安全的,由ConcurrentHashMap保证。但是由于在上面的代码中,map本身是一个共享变量。当线程A执行map.get的时候,其它线程可能正在执行map.put,这样一来当线程A执行到map.put的时候,线程A的值就已经是脏数据了,然后脏数据覆盖了真值,导致线程不安全

简单地说,ConcurrentHashMap的get方法获取到的是此时的真值,但它并不保证当你调用put方法的时候,当时获取到的值仍然是真值

为了使上面的代码变得线程安全,我引入了synchronized关键字来修饰目标方法,如下:

public class Test40 {

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            System.out.println(test());
        }
    }
    
    private static int test() throws InterruptedException {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 8; i++) {
            pool.execute(new MyTask(map));
        }
        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.DAYS);
        
        return map.get(MyTask.KEY);
    }
}

class MyTask implements Runnable {
    
    public static final String KEY = "key";
    
    private ConcurrentHashMap<String, Integer> map;
    
    public MyTask(ConcurrentHashMap<String, Integer> map) {
        this.map = map;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            this.addup();
        }
    }
    
    private synchronized void addup() { // 用关键字synchronized修饰addup方法
        if (!map.containsKey(KEY)) {
            map.put(KEY, 1);
        } else {
            map.put(KEY, map.get(KEY) + 1);
        }
    }
    
}

运行之后仍然是线程不安全的,难道synchronized也失效了?

查阅了synchronized的资料后,原来,不管synchronized是用来修饰方法,还是修饰代码块,其本质都是锁定某一个对象。修饰方法时,锁上的是调用这个方法的对象,即this;修饰代码块时,锁上的是括号里的那个对象

在上面的代码中,很明显就是锁定的MyTask对象本身。但是由于在每一个线程中,MyTask对象都是独立的,这就导致实际上每个线程都对自己的MyTask进行锁定,而并不会干涉其它线程的MyTask对象。换言之,上锁压根没有意义

理解到这点之后,对上面的代码又做了一次修改:

public class Test40 {

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            System.out.println(test());
        }
    }
    
    private static int test() throws InterruptedException {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String, Integer>();
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 8; i++) {
            pool.execute(new MyTask(map));
        }
        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.DAYS);
        
        return map.get(MyTask.KEY);
    }
}

class MyTask implements Runnable {
    
    public static final String KEY = "key";
    
    private ConcurrentHashMap<String, Integer> map;
    
    public MyTask(ConcurrentHashMap<String, Integer> map) {
        this.map = map;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            synchronized (map) { // 对共享对象map上锁
                this.addup();
            }
        }
    }
    
    private void addup() {
        if (!map.containsKey(KEY)) {
            map.put(KEY, 1);
        } else {
            map.put(KEY, map.get(KEY) + 1);
        }
    }
    
}

此时在调用addup时直接锁定map,由于map是被所有线程共享的,因而达到了让所有线程互斥的目的,线程安全达成。

修改后,ConcurrentHashMap的作用就不大了,可以直接将代码中的map换成普通的HashMap,以减少由ConcurrentHashMap带来的锁开销

最后特别补充的是,synchronized关键字判断对象是否是它属于锁定的对象,本质上是通过 == 运算符来判断的。换句话说,上面的代码中,可以采用任何一个常量,或者每个线程都共享的变量,或者MyTask类的静态变量,来代替map。只要该变量与synchronized锁定的目标变量相同(==),就可以使synchronized生效

综上,代码最终可以修改为:

public class Test40 {

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            System.out.println(test());
        }
    }
    
    private static int test() throws InterruptedException {
        Map<String, Integer> map = new HashMap<String, Integer>();
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 8; i++) {
            pool.execute(new MyTask(map));
        }
        pool.shutdown();
        pool.awaitTermination(1, TimeUnit.DAYS);
        
        return map.get(MyTask.KEY);
    }
}

class MyTask implements Runnable {
    
    public static Object lock = new Object();
    
    public static final String KEY = "key";
    
    private Map<String, Integer> map;
    
    public MyTask(Map<String, Integer> map) {
        this.map = map;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            synchronized (lock) {
                this.addup();
            }
        }
    }
    
    private void addup() {
        if (!map.containsKey(KEY)) {
            map.put(KEY, 1);
        } else {
            map.put(KEY, map.get(KEY) + 1);
        }
    }
    
}


本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

ConcurrentHashMap、synchronized与线程安全 的相关文章

  • java中的final变量和synchronized块

    Java中的final变量是什么 例如 如果我写final int temp 函数中final关键字的含义是什么 另外 我什么时候想使用final变量 既作为类变量又作为函数变量 为什么同步块中的变量必须声明为final Final 变量和
  • 更好的解决方案而不是 Java 中的嵌套同步块?

    我有一个Bank类有一个列表Account 该银行有一个transfer 将价值从一个帐户转移到另一个帐户的方法 这个想法是锁定两个from and to转账内的账户 为了解决这个问题 我有以下代码 请记住 这是一个非常简单的示例 因为它只
  • 在函数中通过类名同步是否在扩展类中有效?

    我在基类中有一个方法 foo 使用 Synchronized 类名 以及扩展基类的两个类 A 和 B 如果我在两个不同的线程中从 A 实例和 B 实例调用 foo 它们会同步吗 这是一个示例代码 class BaseClass void f
  • 统计catch块中发生的异常数量

    我正在尝试收集发生异常的所有计数以及异常的名称ConcurrentHashMap这样我就应该知道这个异常发生了多少次 因此 在我的 catch 块中 我有一个映射 它将继续添加异常的名称和出现的总计数 下面是我的代码which I have
  • 为什么Java构造函数不能同步?

    根据Java语言规范 http java sun com docs books jls third edition html classes html 8 8 3 构造函数不能标记为同步 因为在创建该对象的线程完成之前 其他线程无法看到正在
  • ConcurrentHashMap 调整大小时读取

    我想知道当我们尝试在 ConcurrentHashMap 调整大小时读取它时可能发生的情况 我知道在读取期间 第一次尝试总是不同步的 在第二次尝试中 它将尝试获取锁并重试 但如果在调整大小期间发生这种情况 它将如何工作 Thanks 通过查
  • ConcurrentHashMap 有什么缺点吗?

    我需要一个可从多个线程访问的 HashMap 有两个简单的选项 使用普通的 HashMap 并在其上同步 或者使用 ConcurrentHashMap 由于 ConcurrentHashMap 不会阻止读取操作 因此它似乎更适合我的需求 几
  • gcc std::unordered_map 实现速度慢吗?如果是这样 - 为什么?

    我们正在用 C 开发高性能关键软件 我们需要一个并发哈希映射并实现它 因此 我们编写了一个基准测试来弄清楚 我们的并发哈希映射与std unordered map But std unordered map似乎非常慢 所以这是我们的微基准测
  • Java的happens-before和同步

    我对 Java 有一点不同意见发生在之前和同步 想象一下以下场景 主线程 MyObject o new MyObject 0 synchronized sharedMonitor 1 add the object to a shared c
  • 具有空键功能的线程安全映射

    我需要一个多线程 Map 对象在我的 Web 服务器的缓存中使用 并且我需要null keys HashMap允许我有空键 但是ConcurrentHashMap没有 我尝试创建一个同步版本HashMap using Collections
  • BluetoothChat同步了Activity的onResume生命周期方法,为什么?

    我现在正在研究蓝牙 Android API 并且遇到了 BluetoothChat 示例 http developer android com resources samples BluetoothChat index html http
  • Java中的ConcurrentHashMap?

    有什么用ConcurrentHashMap在Java中 它有什么好处 它是如何工作的 示例代码也很有用 重点是提供一个实现HashMap那是线程安全的 多个线程可以读取和写入它 而不会接收到过期或损坏的数据 ConcurrentHashMa
  • 异步映射中的同步部分

    我有一个大的 IO 函数 它将持续从文件夹加载数据 对数据执行纯计算 然后写回 我正在多个文件夹上并行运行此函数 mapConcurrently iofun folderList from http hackage haskell org
  • Java ConcurrentHashMap 集合的模式

    我在多线程应用程序中常用的数据结构是 ConcurrentHashMap 我想在其中保存一组共享相同键的项目 安装特定键值的第一个项目时会出现此问题 我一直使用的模式是 final ConcurrentMap
  • 为什么 java 中 wait/notify/notifyAll 方法不同步?

    在Java中 每当我们需要调用wait notify notifyAll时 我们都需要访问对象监视器 通过synchronized方法或通过synchronized块 所以我的问题是为什么java不采用同步等待 通知方法来消除从同步块或方法
  • 分布式张量流 tf.train.SyncReplicasOptimizer 似乎不同步

    我使用两个工作程序 副本和一个参数服务器 喜欢 ps hosts hosta com 2222 worker hosts hosta com 2223 hostb com 2223 使用tf train SyncReplicasOptimi
  • Java - 同步方法导致程序大幅减慢

    我正在尝试了解线程和同步 我做了这个测试程序 public class Test static List
  • 独占锁定ConcurrentHashMap

    我知道不可能锁定 ConcurrentHashMap 进行独占访问 但是 我找不到原因 是因为构成CHM的 Segment 没有被api公开吗 据推测 如果是的话 客户端代码可以执行 交接 锁定 Cheers 我知道不可能锁定 Concur
  • Java基于参数的同步(名为互斥锁/锁)

    我正在寻找一种根据接收到的参数来同步方法的方法 如下所示 public synchronized void doSomething name some code 我想要方法doSomething同步基于name参数如下 线程 1 doSom
  • 对 java ConcurrentHashMap 中的值进行排序

    我有以下用于对 ConcurrentHashMap 进行排序的代码 ConcurrentHashMap

随机推荐

  • 程序员经典语录

    程序员编程语录 1 一个好的程序员是那种过单行线马路都要往两边看的人 xff08 Doug Linder xff09 2 程序有问题时不要担心 如果所有东西都没问题 xff0c 你就失业了 xff08 软件工程的Mosher定律 xff09
  • 使用fastboot命令刷机流程详解

    一 Fastboot是什么 1 1 首先介绍Recovery模式 卡刷 在系统进行定制时 xff0c 编译系统会编译出一份ZIP的压缩包 xff0c 里面是一些系统分区镜像 xff0c 提供给客户进行手动升级 恢复系统 需要提前将压缩包内置
  • 【谷歌插件】谷歌插件制作

    文章目录 谷歌浏览器插件制作教程实现步骤成功示例问题未封装的扩展程序并非来自 Chrome 网上应用商店 谷歌浏览器插件制作 教程 教程1 xff1a https blog csdn net github 35631540 article
  • 一个刚毕业大学生的四个月苦逼程序员经历

    先来一个自我介绍 大学时排名老三 就暂且叫老三吧 xff0c 毕业于河南的一个还算可以的二本院校 xff0c 专业 地球信息科学与技术 首先介绍一下我的专业 xff0c 听着名字很高大上 xff0c 其实 xff0c 我们都叫他四不像专业
  • JS中的require、import、default、export

    刚开始学的时候经常弄混总结一下 xff1a 懒人 xff1a 1 require xff08 导入 xff09 是Commonjs的规范与module exports xff08 导出 xff09 搭配使用 2 import xff08 导
  • Ubuntu安装python3

    sudo apt get install python3 安装python3 xff0c 安装完之后系统默认还是python2 xff0c 要删除python link文件 sudo rm rf usr bin python 然后建立新连接
  • ubuntu安装shutter出现E:无法修正错误

    使用Ubuntu16 04安装shutter时出现如下错误 通过换源可以解决
  • Ubuntu不能访问Windows分区

    将Windows的快速启动关闭即可解决次问题 在电脑中安装了双系统 xff0c 但有时候在Ubuntu中访问Windows分区会出现如下错误 xff1a 以前出现过这种错误 xff0c 是因为windows系统没有完全关闭 xff0c 当时
  • Ubuntu和Windows双系统时间不对的解决办法

    在使用一系统再切换到另一个系统之后 xff0c 系统时间好像是停留在上次关闭该系统的时间 在网上的解决办法通常是 xff1a sudo gedit etc default rcS xff0c 将UTC 61 yes改成UTC 61 no 但
  • Ubuntu出现依赖关系问题 - 仍未被配置问题

    安装软件包时候出现如下错误 xff1a 但这并不是依赖问题 xff0c 使用sudo apt get f install 无法解决 其实问题是因为这六个软件包没有被完全安装或卸载 在安装其他软件的时候会出现 xff1a 就是指这六个软件 使
  • 熬夜总结!最全的Pycharm常用快捷键大全!

    版权声明 xff1a 本文为博主原创文章 xff0c 遵循 CC 4 0 BY SA 版权协议 xff0c 转载请附上原文出处链接和本声明 本文链接 xff1a https blog csdn net momoda118 article d
  • iOS底层-对象里都有什么

    前言 上篇文章说了iOS中alloc方法是怎么创建对象的 xff0c 以及对象的本质是结构体 接下来继续探究对象的内存分布 xff0c 以及对象的isa是个什么样的结构体 xff0c 存储了哪些信息 对象内存分布 已知系统给对象分配内存是1
  • iOS底层-类的三顾茅庐(一)

    前言 了解完对象的底层 xff0c 知道isa指向的是类对象 那么类 xff08 Class xff09 的本质究竟是什么 xff1f 本文顺序isa的指向 xff0c 探索类的继承链 xff0c 和类对象的结构 xff0c 并且尝试获取方
  • iOS底层-alloc方法之旅

    前言 通过汇编调试和源码分析 xff0c 介绍iOS开发当中alloc方法到底做了什么 追踪 alloc 实例化一个对象往往是通过 xxx alloc init 那么alloc和init的区别是什么 xff1f 将两个方法分开调用 xff0
  • iOS底层-类的三顾茅庐(二)

    前言 上篇文章分析了objc class里存储数据的bits xff0c 了解到方法和属性的存储的位置class rw t xff08 以下简称rw xff09 本文将继续研究rw里包含的其他内容 类数据的存储 书接上文 xff0c rw结
  • iOS底层-类的三顾茅庐(三)

    前言 上文讲解完了类对象的结构体objc class用来存储类信息的成员bits xff0c 整个结构还剩下方法的缓存cache xff0c 放在压轴来讲解 简化版 struct objc class objc object 类对象指针 x
  • iOS底层-消息发送机制

    前言 通过对类的缓存探索了解到方法缓存在类对象的成员cache中 xff0c 而缓存的目的是为了方法调用的时候能更快的进行响应 缓存的时候 xff0c cache t结构体用到insert方法进行插入的 xff0c 那么本次就探索怎么读取
  • iOS底层-消息的转发

    前言 上篇文章介绍了方法调用的本质是消息发送 那如果经过查找后 xff0c 没有找到方法 xff0c 系统会怎么处理 xff1f 这就是本文接下来介绍的方法的动态决议和消息转发 动态决议 当方法查找一直查到父类为nil之后 xff0c 有i
  • 指针地址+1的理解

    指针向后移动一个单位 xff0c 如果是char指针 xff0c 就是1 xff0c 如果是int指针 xff0c 就是4 xff0c 如果是数组 xff0c 还要看是哪一维的下标 xff0c 要加上相应的维 include lt stdi
  • ConcurrentHashMap、synchronized与线程安全

    最近做的项目中遇到一个问题 xff1a 明明用了ConcurrentHashMap xff0c 可是始终线程不安全 除去项目中的业务逻辑 xff0c 简化后的代码如下 xff1a public class Test40 public sta