V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
gramyang
V2EX  ›  Java

终于研究明白了, concurrenthashmap 的 get 然后 put 的并发问题

  •  
  •   gramyang · 2019-06-18 10:48:44 +08:00 · 8048 次点击
    这是一个创建于 1984 天前的主题,其中的信息可能已经有所发展或是发生改变。

    我参考的是这篇文章: https://www.cnkirito.moe/java-ConcurrentHashMap-CAS/ 网上论述这个的文章实在是不多。

    我的测试代码如下:

    public class test2 { private final static Map<String, Table> map = new ConcurrentHashMap<>(); private static final String KEY = "key";

    public static void increase1(String key) {
    	Table oldTable = map.get(key);
    	int value = oldTable.getI();
    	oldTable.setI(value + 1);
    	map.put(key, oldTable);
    }
    
    public static void increase2(String key) {
    	Table oldTable;
    	Table newTable = new Table(0);
    	while(true) {
    		oldTable = map.get(KEY);
    		newTable.setI(oldTable.getI() + 1);
    		if(map.replace(KEY, oldTable, newTable)) break;
    	}
    }
    
    public static int getTableValue(String key) {
    	return map.get(key).getI();
    }
    
    public static void main(String[] args) {
    	map.put(KEY, new Table(0));
    	ExecutorService executor = Executors.newFixedThreadPool(10);
    	int callTime = 1000;
    	CountDownLatch countDownLatch = new CountDownLatch(callTime);
    	for(int i=0; i<callTime; i++) {
    		executor.execute(new Runnable() {
    			@Override
    			public void run() {
    				increase2(KEY);
    				countDownLatch.countDown();
    			}
    		});
    	}
    	try {
    		countDownLatch.await();
    	} catch (InterruptedException e) {
    		e.printStackTrace();
    	}
    	executor.shutdown();
    	System.out.println("调用次数:" + getTableValue(KEY));
    }
    

    }

    class Table { private int i;

    public Table(int i) {
    	this.i = i;
    }
    
    public int getI() {
    	return i;
    }
    
    public void setI(int i) {
    	this.i = i;
    }
    

    }

    测试结果:使用 increase1 的话,调用次数是不停变动的,存在并发错误。而用 increase2 的话,恒定都是 1000.

    15 条回复    2019-09-02 23:30:28 +08:00
    annoymous
        1
    annoymous  
       2019-06-18 10:57:52 +08:00
    我觉着吧 CAS 的文章到处都是 一搜一大把
    WishingFu
        2
    WishingFu  
       2019-06-18 11:07:05 +08:00
    我觉得你这个例子跟 Map 关系不大,例 1 是 table 的 get 和 set 同步问题,map 的 put 没有意义,坐等大佬深入科普学习一波
    v2lf
        3
    v2lf  
       2019-06-18 12:26:02 +08:00
    CHM 只是保证 table 这个引用对所有的线程可见性(保证对象的正确发布),然而 Table 是不是线程安全的,不是 CHM 能控制的。CHM 每次的 put 也只能确保调用 put 方法的线程,刷新 local 内存到主内存(相当于一个类,只同步了 set 方法,没有同步 get 方法,所以这个类不是线程安全的)。increase2 运行正确 是因为你重新实例化了一个对象,相当于 Table 是事实不可变对象。
    v2lf
        4
    v2lf  
       2019-06-18 12:29:00 +08:00
    另外 我个人建议, 网上很多文章都是借鉴来借鉴去··根据我看的经验来说,无法保证正确性,有些文章的描述,是老的 JMM,但是新的 JMM 已经增强了一些同步 y 元语,所以我的建议是看书 以及看 JUC 源码。
    [!]( http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#jsr133)
    书看 java 并发实战把 大神的那本书
    tslling
        5
    tslling  
       2019-06-18 12:58:11 +08:00 via Android
    你变动的是 CHM 里 value 的内部状态怎么能怪 CHM 呢? CHM 只保证这个 value 的同步呀,这里 table 不是线程安全的 CHM 也没办法
    alamaya
        6
    alamaya  
       2019-06-18 13:17:31 +08:00
    我觉得这篇文章写得没问题,你的理解可能有点问题
    chendy
        7
    chendy  
       2019-06-18 13:40:15 +08:00
    这里 map 的逻辑完全没用,就是证明了一下 Table 线程不安全…
    gramyang
        8
    gramyang  
    OP
       2019-06-18 14:00:50 +08:00
    @chendy
    @tslling
    @v2lf
    @WishingFu
    不要一看到 map 的 put 行为就两眼放光大呼你肯定是个小白,我当然知道非基础类型的引用是指针,不需要 put 回去。一点笔误而已。

    为什么要用 table ?首先你实际代码中用 concurrenthashmap,value 基本上不可能是基本类型或者包装类,都是复合类,用个 table 包装一下再正常不过了。所以网上那种用 AtomicInteger 来实现写安全的基本上没有什么实际意义。

    我这个帖子主要验证的是在 while 循环中 replace 成功后 break 的写法,针对的是 put 后 get 的问题,这应该是每个接触 concurrenthashmap 的人都踩过的坑吧??我不觉得这个发现烂大街,一点意义都没有。

    replace 方法的问题在于两个参数必须是 value 类型,实际使用中的 value 都是复合类型,不可能是基本类型或者是包装类,所以我加个 table 来验证一下。然而这个时候又涉及到了深拷贝,不过我这里没有体现。
    v2lf
        9
    v2lf  
       2019-06-18 14:15:05 +08:00
    @gramyang 大佬 我可没有直呼你是小白,我只是说了我的认为··· 算了, 不讨论了,安心看书···
    wysnylc
        10
    wysnylc  
       2019-06-18 14:15:58 +08:00
    分布式环境下,公共变量不用 redis 的都是坑嗷
    本地(单机)并行处理当我没说
    firefffffffffly
        11
    firefffffffffly  
       2019-06-18 14:21:42 +08:00   ❤️ 1
    @gramyang

    每个接触 concurrenthashmap 的人都踩过的坑 ×
    不理解线程安全的人踩过的坑 √

    increase1 的线程不安全发生在 int value = oldTable.getI(); oldTable.setI(value + 1);这两句上
    increase2 使用 CAS 乐观锁的方式解决了 increase1 里线程不安全的问题,你也可以用传统的 synchornized 悲观锁同样解决问题。
    无论怎样都没有涉及到 CHM 的任何问题。
    micean
        12
    micean  
       2019-06-18 14:23:33 +08:00
    多线程操作一个 object,思路不应该是保证这个操作过程是线程安全的么,跟 map 没什么关系啊
    像这样
    class Table{
    void updateSafe();
    }

    map.get(KEY).updateSafe();
    gramyang
        13
    gramyang  
    OP
       2019-06-18 14:38:22 +08:00
    @firefffffffffly 是的,我确实对线程安全的理解不够深入
    zazalu
        14
    zazalu  
       2019-06-23 13:16:56 +08:00
    希望可以帮忙看下我这个问题,https://www.v2ex.com/t/576609#reply0

    从楼主的问题引发出来的后续问题
    tslling
        15
    tslling  
       2019-09-02 23:30:28 +08:00
    @gramyang 我没有两眼放光。我没有大呼你肯定是个小白。不知道你有没有看到,我觉得 12 楼的思路更好,去确保 value 类型的线程安全性。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   4955 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 03:52 · PVG 11:52 · LAX 19:52 · JFK 22:52
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.