并发容器是线程安全的,但只是指并发容器的原子操作是线程安全的,多个原子操作组成的复合操作仍然是不安全的,需要通过加锁来保证多线程的安全。
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String,Integer>();
map.put("key", 1);
ExecutorService executorService = Executors.newFixedThreadPool(100);
for (int i = 0; i < 1000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
int key = map.get("key") + 1; //step 1
map.put("key", key);//step 2
}
});
}
如上图step12所示,线程A获取了并发容器的值,执行加一操作,还没写入并发容器中,线程B同时去获取该值,会导致读取到未更新的值。
concurrentHashMap基本数据结构
ConcurrentHashMap在1.8中的实现,相比于1.7的版本基本上全部都变掉了。首先,取消了Segment分段锁的数据结构,取而代之的是数组+链表(红黑树)的结构。而对于锁的粒度,调整为对每个数组元素加锁(Node)(说明:Node不支持修改值,修改会抛出异常)。然后是定位节点的hash算法被简化了,这样带来的弊端是Hash冲突会加剧。因此在链表节点数量大于8时(小于6会转回链表),会将链表转化为红黑树进行存储(如果当前Node数组长度小于阈值MIN_TREEIFY_CAPACITY,默认为64,先通过扩大数组容量为原来的两倍以缓解单个链表元素过大的性能问题)。这样一来,查询的时间复杂度就会由原先的O(n)变为O(logN)。默认容量是16,可自定义初始化容量作为构造方法的参数。
在Java中,悲观锁的实现方式就是各种锁;而乐观锁则是通过CAS实现的。
CAS(Compare And Swap,比较交换):CAS有三个操作数,内存值V、预期值A、要修改的新值B,当且仅当A和V相等时才会将V修改为B,否则什么都不做。Java中CAS操作通过JNI本地方法实现,在JVM中程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(Lock Cmpxchg);反之,如果程序是在单处理器上运行,就省略lock前缀。
不过CAS操作也存在一些缺点:1. 存在ABA问题,其解决思路是使用版本号
扩容过程
扩容过程中,正在扩容的线程会将正在转移的table节点标记为ForwardingNode,其他线程若是查找到某个节点为ForwardingNode类型节点,则查找下一个talbe节点辅助进行扩容操作,转移完一个节点,直接将该节点设置为fwd,表示已经遍历过。每个线程负责转移的数组区间最少为16,避免发生大量冲突,也就是每个线程负责连续的16个table节点的转移。nextTable指向新数组,扩容完成后nextTable置null,table指向新数组,
size() 方法获取当前Map中对象中的键值对个数时,返回的是估算值,不是精确值。
get()操作没有加锁,说明是弱一致性。
copyOnWriteArrayList