超话Java----HashMap和ConcurrentHashMap
HashMap和ConcurrentHashMap在不同版本之间的差别
JDK1.7
- 数据结构
HashMap采用Entry数组 + 链表的结构
ConcurrentHashMap采用segment数组+Entry数组+链表的结构
- 查询复杂度:O(n)
- 插入数据的方式:头插法(先将原数据向后移动在插入当前数据)
- 扩容的时机:插入数据之前扩容
- 扩容的方式:全部按照原来的方法重新计算
JDK1.8
- 数据结构
HashMap采用Entry数组 + 链表 / 红黑树的结构
ConcurrentHashMap采用Node数组 + 链表 / 红黑树的结构 - 查询复杂度:O(n) 或 O(logN)
- 插入数据的方式:尾插法(直接插入到链表尾部/红黑树)
- 扩容的时机:插入数据成功后扩容
- 扩容的方法:没有重Hash计算,直接高低位取模
没有重hash,而是采用4个指针(高低位头尾指针)直接放到index(原位置)和(index + oldlength)的位置,(为什么没有rehash?因为每个节点的数据直接&oldlength做与运算可以得到两个不同的值一个index和一个(index + oldlength)就可以直接获取到位置。)
HashMap的各个参数
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始容量
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子
static final int TREEIFY_THRESHOLD = 8; // 树化阈值(链表长度达到8时转换成红黑树)
static final int UNTREEIFY_THRESHOLD = 6; // 链化阈值(红黑树中的长度减少到6时转换成链表)
static final int MIN_TREEIFY_CAPACITY = 64; // 最小的树化Node数组的长度(即:Node数组的长度要达到64才会转化成红黑树)
为什么树化阈值是8?
- 遵循泊松分布概率,8个数据定位到同一个数组节点概率最小。(HashMap 源码中的解释)
加载因子为什么是0.75?
- 加载因子是数据储存达到数组长度的0.75就扩容
- 减少hash碰撞 时间与空间之间做平衡,选择0.75
- 加载因子为1 最大化利用空间
- 加载因子为0.5 查询效率高 时间复杂度快 但是空间浪费多
HashMap的put逻辑
- 如果HashMap未被初始化过,则初始化(延迟创建)
- 对Key进行与运算求Hash值,然后在计算下标
- 如果没有Hash碰撞,直接放入Node桶中
- 如果碰撞了,以链表的方式连接到后面
- 如果链表长度超过阈值(8),就把链表转成红黑树
- 如果链表长度低于6,就把红黑树转回链表
- 如果节点已经存在就替换旧值
- 如果桶满了(容量16*加载因子0.75 == 12),就需要resize(扩容2倍后重排)
HashMap如何有效减少碰撞?
- 扰动函数:促使元素位置分布均匀,减少碰撞几率
- 使用final对象,并采用合适的equals()和hashCode()方法
不可变性使得能够缓存不同键的HashCode,这将提高获取对象的速度(String,Integer)
为什么HashMap是线程不安全的?
1. 同时put碰撞导致数据丢失
两个线程同时put到同一个桶(Node[]的一个节点)中(put()方法的逻辑是先将桶拿出来插入新值在放回去),两个线程同时拿到先前的桶,这样就导致一个数据必定丢失
2. 同时put扩容到时数据丢失
两个线程同时发现需要扩容,那么最后也是会导致一个线程要put进去的数据丢失。(与上面的类似)
3. 死循环造成的CPU100%
在并发的情况,发生扩容时,可能会产生循环链表,在执行get的时候,会触发死循环,引起CPU的100%问题,所以一定要避免在并发环境下使用HashMap。
HashMap关于并发的特点
- 非线程安全
- 迭代时不允许修改内容
- 只读的并发是线程安全的
- 如果一定要把HashMap用在并发环境中就用Collections.synchronizedMap(new HashMap<>());
ConcurrentHashMap在JDK1.7和JDK1.8中的锁机制
JDK1.7
- 1.7的ConcurrentHashMap是采用segment数组 + Entry数组 + 链表的结构
- 每个segment数组会有一个锁,形成分段锁(segment类是继承了ReentrantLock自带锁的功能)每个segment之间互不影响,提高了并发效率
- 每个segment的底层数据结构与HashMap类似,仍然是数组和链表组成的拉链法
- ConcurrentHashMap默认有16个Segments,所以最多同时支持16个线程并发写(操作分别分布在不同的segment上)。这个默认值可以在初始化的时候设置为其他值,但是一旦初始化以后,是不可以扩容的。
JDK1.8
- 1.8时 ConcurrentHashMap采用Node数组 + 链表或红黑树的结构
- 采用CAS + synchronized保证并发安全
- synchronized锁的对象是Node数组里面的根节点
- 1.8 将锁的粒度更加细化,且支持的并发数更多,可以扩容
- 插入数据时,先判断Node数组是否存在,如果不存在则采用CAS插入头结点,失败则循环重试;如果存在则尝试获取头结点的同步锁,再进行操作。
ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?
- 粒度降低了
- JVM 开发团队没有放弃 synchronized,而且基于 JVM 的 synchronized 优化空间更大,更加自然。
- 在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。
HashMap、Hashtable、ConcurrentHashMap 三者区别
- HashMap线程不安全,数组+链表+红黑树
- Hashtable线程安全,锁住整个对象,数组+链表
- ConcurrentHashMap线程安全。CAS+同步锁synchronized,数组+链表+红黑树
- HashMap的key、value均可为null,而其他两个类不支持