首先需要明确的是,不管使用那种Map,都不能保证公共混合调用的线程安全,只能保证单条操作的线程安全,在这一点上各Map不存在优劣。

前文中简单说过HashTable和synchronizedMap,其实这两个类不需要说太多,把代码贴一下相信看过Java多线程的就能很容易理解了。

HashTable

HashTable的话,实现这个样子的。可以看到的是,对于Hash表的所有操作,HashTable都加了锁,但也只能保证单条操作的线程安全。

public synchronized V get(Object key) {
// 省略实现
}
public synchronized V put(K key, V value) {
// 省略实现
}

synchronizedMap

synchronizedMap的实现如下,没直接在方法上加,尽管其实质与HashTable是等效的,也同样有HashTable的缺陷,但synchronizedMap给用户留下了选择的空间:用户可以在不需要加锁时直接操作原始Map,在实际编码时就可以基于这点进行优化。

// synchronizedMap方法
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {
return new SynchronizedMap<>(m);
}
// SynchronizedMap类
private static class SynchronizedMap<K,V>
implements Map<K,V>, Serializable {
private static final long serialVersionUID = 1978198479659022715L; private final Map<K,V> m; // Backing Map
final Object mutex; // Object on which to synchronize SynchronizedMap(Map<K,V> m) {
this.m = Objects.requireNonNull(m);
mutex = this;
} SynchronizedMap(Map<K,V> m, Object mutex) {
this.m = m;
this.mutex = mutex;
} public int size() {
synchronized (mutex) {return m.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return m.isEmpty();}
}
public boolean containsKey(Object key) {
synchronized (mutex) {return m.containsKey(key);}
}
public boolean containsValue(Object value) {
synchronized (mutex) {return m.containsValue(value);}
}
public V get(Object key) {
synchronized (mutex) {return m.get(key);}
} public V put(K key, V value) {
synchronized (mutex) {return m.put(key, value);}
}
public V remove(Object key) {
synchronized (mutex) {return m.remove(key);}
}
// 省略其他方法
}

ConcurrentHashMap

提高安全HashMap的并发性的方法,可以通过减小锁粒度的方式,不对整个Hash表加锁,而是对每个bucket加锁,甚至用锁池,每个锁维护几个bucket,让Map的不同部分可以被多个线程访问,不过这样的方式会让对整体集合操作的方法的实现更加困难。Java7中的ConcurrentHashMap就通过Segment引入了这个分段加锁概念,但Java8由于上述困难更改了机制,引入了红黑树结构,去掉了Segment。

JDK1.8的改进后,ConcurrentHashMap的写性能有10%左右的降低,但读性能有了很大提升。主要是将过于集中的hash节点的效率从O(N)提高到了O(LOGN)。

ConcurrentHashMap利用了CAS进行实现,从而以乐观锁的方式实现了线程安全的HashMap,concurrentHashMap的源码很复杂,一些方法的实现思路如下:

Java8的ConcurrentHashMap的数据结构实现思路大概为,对于Hash表中每一个节点,其数据结构可以为单节点,链表数组或红黑树,随着节点中元素增加而改变。(改变方法见treeifyBin)。

put()方法

  • hash数组是否为空,为空则先调用initTable()方法进行初始化
  • 如果hash数组已经初始化了,则根据hash值找到对应的数组下标,如果对应节点为空,通过cas方式直接插入
  • 如果数组已经扩容,则进行数据迁移
  • 如果数组该位置已经有值了,则需要对该节点加锁并进行数据插入操作,仅对一个节点加锁,其锁粒度实际上比Java7中Segment实现更小。此时如果该节点是链表结构,则遍历链表,插入数据;如果如果该节点是红黑树结构,则调用红黑树的插值方法插入新值
  • 针对链表结构,如果插入新元素后,hash数组长度超过阈值,则需要调用treeifyBin()方法进行扩容或者是将链表转换为红黑树

initTable()方法

  • 当table不存在,开始自旋。
  • 利用CAS操作将sizeCtl属性设置为-1,表示本线程正对数组初始化,阻止其他线程的初始化。
  • 进行常规的初始化操作,扩容阈值为数组容量的75%。
  • 将sizeCtl设置成扩容阈值,结束初始化。

treeifyBin()方法

该方法用于对数组链表扩容,或将链表结构转化为红黑树,一个节点的元素个数大于链表阈值(默认8)时,如果数组链表长度小于红黑树阈值(默认64),则对数组链表扩容,否则将该节点转换为红黑树。

transfer(),helpTransfer(),tryPresize()方法

这些方法负责hash表扩容,由于要通过CAS实现线程安全,代码十分复杂。大概思路为,原数组长度为n,则产生n个迁移任务,让每一个线程负责一个小任务,之后监测是否有其他没做完的任务,帮助迁移。

get()方法

get方法不涉及CAS操作,实现较为简单,计算hash值,找到对应节点进行判断:

  • 该位置为null返回null。
  • 该位置节点为所求值,返回值。
  • 该位置节点hash值小于0,说明在扩容,或者为红黑树,使用find方法。
  • 以上都不满足,该位置为链表,遍历搜索。

性能

目前多线程环境下ConcurrentMap的性能有很高的优越性,通常情况下,如果你的Map处于多读少写的场景,优先考虑ConcurrentMap,但在多写少读的情境中,由于资源竞争激烈,CAS自旋可能导致ConcurrentMap性能不如synchronizedMap。

参考文献

Collections.synchronizedMap()、ConcurrentHashMap、Hashtable之间的区别

Java8 ConcurrentHashMap详解

浅谈Java8中的ConcurrentHashMap

SynchronizedMap

Java容器:HashTable, synchronizedMap与ConcurrentHashMap的更多相关文章

  1. Java中SynchronizedMap与ConcurrentHashMap的对比

    如何使用 概述 ConcurrentHashMap: 线程安全: 其将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都 ...

  2. 测试HashTable、Collections.synchronizedMap和ConcurrentHashMap的性能

        对于map的并发操作有HashTable.Collections.synchronizedMap和ConcurrentHashMap三种,到底性能如何呢? 测试代码: package com. ...

  3. Hashtable、synchronizedMap、ConcurrentHashMap 比较

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcytp18 Hashtable.synchronizedMap.Concurren ...

  4. Collections.synchronizedMap()、ConcurrentHashMap、Hashtable之间的区别

    为什么要比较Hashtable.SynchronizedMap().ConcurrentHashMap之间的关系?因为常用的HashMap是非线程安全的,不能满足在多线程高并发场景下的需求. 那么为什 ...

  5. 沉淀再出发:java中的HashMap、ConcurrentHashMap和Hashtable的认识

    沉淀再出发:java中的HashMap.ConcurrentHashMap和Hashtable的认识 一.前言 很多知识在学习或者使用了之后总是会忘记的,但是如果把这些只是背后的原理理解了,并且记忆下 ...

  6. Java - 容器详解

    一.ArrayList 长度可变数组,类似于c++ STL中的vector. 元素以线性方式连续存储,内部允许存放重复元素. 允许对元素进行随机的快速访问,但是向ArrayList中插入和删除元素的速 ...

  7. SynchronizedMap和ConcurrentHashMap 区别

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt215 SynchronizedMap和ConcurrentHashMap的深 ...

  8. Java容器的常见问题

    记录Java容器中的常见概念和原理 参考: https://github.com/wangzhiwubigdata/God-Of-BigData#三Java并发容器 https://blog.csdn ...

  9. JAVA学习:HashMap 和 ConcurrentHashMap

     一.最基本的HashMap 和 ConcurrentHashMap 1.HashMap的结构和底层原理:由数组和链表组成,数组里面每个地方都存了Key-Value这样的实例,在Java7叫Entry ...

随机推荐

  1. 001_vagrant利器

    一. 这是一个关于Vagrant的学习系列,包含如下文章: Vagrant入门 http://www.cnblogs.com/davenkin/p/vagrant-virtualbox.html 创建 ...

  2. php时间戳与日期转换

    日期转换为时间戳 PHP 提供了函数可以方便的将各种形式的日期转换为时间戳,该类函数主要是: strtotime():将任何英文文本的日期时间描述解析为时间戳. mktime():从日期取得时间戳. ...

  3. Github之协同开发

    一.协同开发 1.引子:假如三个人共同开发同一份代码,每个人都各自安排了任务,当每个人都完成了一半的时候,提交不提交呢? 要提交,提交到dev吗,都上传了一半,这样回家拿出来的代码根本跑不起来.所以, ...

  4. PHP对Url中的汉字进行编码和解码

    有的新手朋友们对于url编码解码这个概念,或许有点陌生.但是如果这么说,当我们在浏览各大网页时,可能发现有的url里有一些特殊符号比如#号,&号,_号或者汉字等等,那么为了符合url的规范,存 ...

  5. python调用修改变量新方法

    def foo(): count = [1] #将变量放在列表中,此时,内部函数就可以修改了 def bar(): count[0] = count[0] + 1 return count[0] re ...

  6. 集合List和ArrayList的示例

    package chapter09; import java.util.ArrayList;import java.util.List; /* * List * ArrayList底层是数组 * 特点 ...

  7. 《剑指offer》-铺地砖方案数

    我们可以用21的小矩形横着或者竖着去覆盖更大的矩形.请问用n个21的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法? 又是斐波那契...稍微变形一下. class Solution { pub ...

  8. VS2013 生成时复制文件或目录到指定目录

    需求: 在vs2010生成成功时将项目Lib目录下Reader文件夹内的所有文件和文件夹复制到输出目录(Debug或Release目录) 方法: 打开VS2010,右键项目属性-生成事件-后期生成事件 ...

  9. POJ 2456 3258 3273 3104 3045(二分搜索-最大化最小值)

    POJ 2456 题意 农夫约翰有N间牛舍排在一条直线上,第i号牛舍在xi的位置,其中有C头牛对牛舍不满意,因此经常相互攻击.需要将这C头牛放在离其他牛尽可能远的牛舍,也就是求最大化最近两头牛之间的距 ...

  10. 配置apache和php mysql的一些问题

    关于"Windows不能在本地计算机启动Apache2.......并参考特定服务错误代码1"问题解决 apache的httpd.conf文件配置“LoadModule php5_ ...