我参考的是这篇文章: 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 1
@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
|