
我参考的是这篇文章: 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.
1 annoymous 2019-06-18 10:57:52 +08:00 我觉着吧 CAS 的文章到处都是 一搜一大把 |
2 WishingFu 2019-06-18 11:07:05 +08:00 我觉得你这个例子跟 Map 关系不大,例 1 是 table 的 get 和 set 同步问题,map 的 put 没有意义,坐等大佬深入科普学习一波 |
3 v2lf 2019-06-18 12:26:02 +08:00 CHM 只是保证 table 这个引用对所有的线程可见性(保证对象的正确发布),然而 Table 是不是线程安全的,不是 CHM 能控制的。CHM 每次的 put 也只能确保调用 put 方法的线程,刷新 local 内存到主内存(相当于一个类,只同步了 set 方法,没有同步 get 方法,所以这个类不是线程安全的)。increase2 运行正确 是因为你重新实例化了一个对象,相当于 Table 是事实不可变对象。 |
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 并发实战把 大神的那本书 |
5 tslling 2019-06-18 12:58:11 +08:00 via Android 你变动的是 CHM 里 value 的内部状态怎么能怪 CHM 呢? CHM 只保证这个 value 的同步呀,这里 table 不是线程安全的 CHM 也没办法 |
6 alamaya 2019-06-18 13:17:31 +08:00 我觉得这篇文章写得没问题,你的理解可能有点问题 |
7 chendy 2019-06-18 13:40:15 +08:00 这里 map 的逻辑完全没用,就是证明了一下 Table 线程不安全… |
8 gramyang OP @chendy @tslling @v2lf @WishingFu 不要一看到 map 的 put 行为就两眼放光大呼你肯定是个小白,我当然知道非基础类型的引用是指针,不需要 put 回去。一点笔误而已。 为什么要用 table ?首先你实际代码中用 concurrenthashmap,value 基本上不可能是基本类型或者包装类,都是复合类,用个 table 包装一下再正常不过了。所以网上那种用 AtomicInteger 来实现写安全的基本上没有什么实际意义。 我这个帖子主要验证的是在 while 循环中 replace 成功后 break 的写法,针对的是 put 后 get 的问题,这应该是每个接触 concurrenthashmap 的人都踩过的坑吧??我不觉得这个发现烂大街,一点意义都没有。 replace 方法的问题在于两个参数必须是 value 类型,实际使用中的 value 都是复合类型,不可能是基本类型或者是包装类,所以我加个 table 来验证一下。然而这个时候又涉及到了深拷贝,不过我这里没有体现。 |
10 wysnylc 2019-06-18 14:15:58 +08:00 分布式环境下,公共变量不用 redis 的都是坑嗷 本地(单机)并行处理当我没说 |
11 firefffffffffly 2019-06-18 14:21:42 +08:00 @gramyang 每个接触 concurrenthashmap 的人都踩过的坑 × 不理解线程安全的人踩过的坑 √ increase1 的线程不安全发生在 int value = oldTable.getI(); oldTable.setI(value + 1);这两句上 increase2 使用 CAS 乐观锁的方式解决了 increase1 里线程不安全的问题,你也可以用传统的 synchornized 悲观锁同样解决问题。 无论怎样都没有涉及到 CHM 的任何问题。 |
12 micean 2019-06-18 14:23:33 +08:00 多线程操作一个 object,思路不应该是保证这个操作过程是线程安全的么,跟 map 没什么关系啊 像这样 class Table{ void updateSafe(); } map.get(KEY).updateSafe(); |
13 gramyang OP @firefffffffffly 是的,我确实对线程安全的理解不够深入 |
14 zazalu 2019-06-23 13:16:56 +08:00 |