都说concurrentHashMap是个线程安全的并发容器,所以没有显示加同步,实际效果呢并不如所愿。
问题就出在increase方法,concurrentHashMap能保证的是每一个操作(put,get,delete…)本身是线程安全的,但是我们的increase方法,对concurrentHashMap的操作是一个组合,先get再put,所以多个线程的操作出现了覆盖。如果对整个increase方法加锁,那么又违背了我们使用并发容器的初衷,因为锁的开销很大。我们有没有方法改善统计方法呢?
代码清单2罗列了concurrentHashMap父接口concurrentMap的一个非常有用但是又常常被忽略的方法。
代码清单2:
/**
* Replaces the entry for a key only if currently mapped to a given value.
* This is equivalent to
* <pre> {@code
* if (map.containsKey(key) && Objects.equals(map.get(key), oldValue)) {
* map.put(key, newValue);
* return true;
* } else
* return false;
* }</pre>
*
* except that the action is performed atomically.
*/
boolean replace(K key, V oldValue, V newValue);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
这其实就是一个最典型的CAS操作,except that the action is performed atomically.
这句话真是帮了大忙,我们可以保证比较和设置是一个原子操作,当A线程尝试在increase时,旧值被修改的话就回导致replace失效,而我们只需要用一个循环,不断获取最新值,直到成功replace一次,即可完成统计。
改进后的increase方法如下
代码清单3:
public long increase2(String url) {
Long oldValue, newValue;
while (true) {
oldValue = urlCounter.get(url);
if (oldValue == null) {
newValue = 1l;
//初始化成功,退出循环
if (urlCounter.putIfAbsent(url, 1l) == null)
break;
//如果初始化失败,说明其他线程已经初始化过了
} else {
newValue = oldValue + 1;
//+1成功,退出循环
if (urlCounter.replace(url, oldValue, newValue))
break;
//如果+1失败,说明其他线程已经修改过了旧值
}
}
return newValue;
}
console output:
调用次数:100000
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
再次调用后获得了正确的结果,上述方案看上去比较繁琐,因为第一次调用时需要进行一次初始化,所以多了一个判断,也用到了另一个CAS操作putIfAbsent,他的源代码描述如下:
size操作
在ConcurrentHashMap获取map中的元素是一个估计值,因为在计算元素的个数的时候可能有其它线程对map进行增删操作。Java中提供了两种获取元素个数的方法:size方法以及mappingCount方法。其中mappingCount方法是JDK8增加的,根据Java API文档,这个方法应该代替size方法使用,因为ConcurrentHashMap可能包含映射的数量超过int所能表示的最大数量,其返回值是long类型: