public class IdGeneratorService {
private final Map<String, AtomicLong> map = new ConcurrentHashMap<>();
public long nextId(String key) {
// 虽然采用了并发安全的容器,但是当 contains 语句通过后,有可能出现多线程先后 put,AtomicLong 值有可能给覆盖?
if (!map.containsKey(key)) {
AtomicLong atomicLong = new AtomicLong(0);
map.put(key, atomicLong);
return atomicLong.incrementAndGet();
}
return map.get(key).incrementAndGet();
}
}
代码如上,如果并发调用 nextId(),我感觉即使使用了并发安全的容器,实际上这段代码也不是线程安全的,如果多线程访问,还是会出现 nextId()重复的问题,有可能 nextId 会出现多个 1 ?但是实际经过测试,并不会重现这个问题。。请教一下,这段代码是不是线程安全的,是否会生成重复 id?
测试代码
public static void main(String[] args) throws InterruptedException {
int count=2000;
CountDownLatch cdl=new CountDownLatch(count);
IdGeneratorService service = new IdGeneratorService();
Map<Long, AtomicLong> countMap=new ConcurrentHashMap<>();
for(long i=1;i<=count;i++){
countMap.put(i,new AtomicLong());
}
for(int i=0;i<count;i++){
new Thread(()->{
long id = service.nextId("test");
countMap.get(id).incrementAndGet();
cdl.countDown();
}).start();
}
cdl.await();
boolean match = countMap.values().stream().mapToLong(AtomicLong::get).anyMatch(l->l>1);
System.out.printf("id 重复=%b\n",match);
}
感谢各位!
其实我的疑惑点是,我理解这段代码是会有线程问题的,但是我写的测试方法却没有测出来。
我后面重复运行了10来次测试方法,能测出id重复的情况。
结帖。
1
JeromeCui 2022-05-20 16:37:45 +08:00
public class IdGeneratorService {
private final Map<String, AtomicLong> map = new ConcurrentHashMap<>(); public long nextId(String key) { if (!map.containsKey(key)) { synchronized{ if (!map.containsKey(key)) { AtomicLong atomicLong = new AtomicLong(0); map.put(key, atomicLong); } } } return map.get(key).incrementAndGet(); } } |
2
JeromeCui 2022-05-20 16:38:06 +08:00
```
public class IdGeneratorService { private final Map<String, AtomicLong> map = new ConcurrentHashMap<>(); public long nextId(String key) { if (!map.containsKey(key)) { synchronized{ if (!map.containsKey(key)) { AtomicLong atomicLong = new AtomicLong(0); map.put(key, atomicLong); } } } return map.get(key).incrementAndGet(); } } ``` |
3
justNoBody 2022-05-20 16:39:25 +08:00
我理解这个和`ConcurrentHashMap`没有关系,因为你用的`incrementAndGet`方法使用了 CAS ,即便是多个线程都同时拿到了这个`AtomicLong`的实例也没有关系
|
4
Georgedoe 2022-05-20 16:40:49 +08:00
同一个 key 有可能会被 put 多次 , 某个 key 的 contains 和 put 不是原子操作 , 可以去看看 go 的 singleflight 的实现 , 保证一次只有一个线程执行了 set (put) 操作
|
5
JeromeCui 2022-05-20 16:42:02 +08:00
完了,格式错乱了
|
7
wolfie 2022-05-20 16:47:52 +08:00
1. ID 不重复是因为 AtomicLog 。
2. 初始化 test 小概率重复创建,直接用 computeIfAbsent 。 |
8
agzou OP @justNoBody #3 但是这两句
if (!map.containsKey(key)) { AtomicLong atomicLong = new AtomicLong(0); map.put(key, atomicLong); return atomicLong.incrementAndGet(); } 有可能返回不同的两个 AtomicLong,这样调用 atomicLong.incrementAndGet(),应该会重复返回 1 ,但是我运行我的测试代码并没有重复 id |
9
Georgedoe 2022-05-20 16:50:23 +08:00
在你代码里加了点 log , 这是输出 , 很显然有问题
public long nextId(String key) { // 虽然采用了并发安全的容器,但是当 contains 语句通过后,有可能出现多线程先后 put,AtomicLong 值有可能给覆盖? if (!map.containsKey(key)) { AtomicLong atomicLong = new AtomicLong(0); System.out.println("put twice"); map.put(key, atomicLong); long l = atomicLong.incrementAndGet(); System.out.println(l); return l; } return map.get(key).incrementAndGet(); } put twice put twice put twice 1 1 2 |
10
Kotiger 2022-05-20 16:52:20 +08:00
正如四楼大佬所说,contains 和 put 组合在一起就不是安全操作了
public class IdGeneratorService { private final Map<String, AtomicLong> map = new ConcurrentHashMap<>(); public long nextId(String key) { // 直接用这个方法 map.computeIfAbsent(key, it->new AtomicLong(0)); return map.get(key).incrementAndGet(); } } |
11
BBCCBB 2022-05-20 16:56:28 +08:00
用 computeIfAbsent ,
有更复杂的场景, 就用 compute 方法, 不过这个方法更加的复杂 |
12
BBCCBB 2022-05-20 16:57:20 +08:00
你这完美避开了 concurrentHashMap 的特性.
|
13
justNoBody 2022-05-20 17:20:45 +08:00
@agzou 你的测试代码和你的`nextId()`方法逻辑是不同的,我不是很理解你具体想要问啥。
|
14
documentzhangx66 2022-05-21 07:47:08 +08:00
资源的并行安全,本质是操作该资源的业务逻辑,在并行中要保证唯一与串行。
当业务逻辑的唯一与串行,能够用 cas api 时,才会出现一行 cas api 语句就够了,比如经典的对同一个资源的 read & set 、compare & set 等等。 但很多业务逻辑,可能需要同时操作不同资源、或者有其他复杂的操作逻辑,此时就不能用 cas 了,而应该老老实实的串行化(锁定)代码段。 |
15
ihuotui 2022-05-21 09:12:55 +08:00
没有深刻理解原子操作含义,如果理解了就不会有疑问。
|