飞利浦·斯塔克|厉害了!阿里大佬把 HashMap 剖析得只剩渣渣了( 四 )


可以看到默认锁的就是本身 , 效果和Hashtable其实是一样的 。 这种简单粗暴锁整个对象的方式造成的后果是:

  • 锁是非常重量级的 , 会严重影响性能 。
  • 同一时间只能有一个线程进行读写 , 限制了并发效率 。
ConcurrentHashMap的设计就是为了解决此问题 。 他通过降低锁粒度+CAS的方式来提高效率 。 简单来说 , ConcurrentHashMap锁的并不是整个对象 , 而是一个数组的一个节点 , 那么其他线程访问数组其他节点是不会互相影响 , 极大提高了并发效率;同时ConcurrentHashMap的操作并不需要获取锁 , 如下图:
关于ConcurrentHashMap和Hashtable的更多内容 , 限于篇幅 , 我会在另一篇文章讲解 。
那么 , 使用了上述的三种解决方案是不是绝对线程安全?先观察下面的代码:
当Thread1调用containsKey之后释放锁 , Thread2获得锁并把“abc”移除再释放锁 , 这个时候Thread1读取到的s就是一个null了 , 也就出现了问题了 。 所以ConcurrentHashMap类或者
Collections.synchronizeMap()方法或者Hashtable都只能在一定的限度上保证线程安全 , 而无法保证绝对线程安全 。
关于线程安全 , 还有一个fast-fail问题 , 即快速失败 。 当使用HashMap的迭代器遍历HashMap时 , 如果此时HashMap发生了结构性改变 , 如插入新数据、移除数据、扩容等 , 那么Iteractor会抛出fast-fail异常 , 防止出现并发异常 , 在一定限度上保证了线程安全 。 如下源码:
创建Iteractor对象时会记录HashMap的modCount变量 , 每当HashMap发生结构性改变时 , modCount会加1 。 在迭代时判断HashMap的modCount和自己保存的expectedModCount是否一致即可判断是否发生了结构性改变 。
fast-fail异常只能当做遍历时的一种安全保证 , 而不能当做多线程并发访问HashMap的手段 。 若有并发需求 , 还是需要使用上述的三种方法 。
小结
  1. HashMap并不能保证线程安全 , 在多线程并发访问下会出现意想不到的问题 , 如数据丢失等
  2. HashMap1.8采用尾插法进行扩容 , 防止出现链表环导致的死循环问题
  3. 解决并发问题的的方案有Hashtable、Collections.synchronizeMap()、ConcurrentHashMap 。 其中最佳解决方案是ConcurrentHashMap
  4. 上述解决方案并不能完全保证线程安全
  5. 快速失败是HashMap迭代机制中的一种并发安全保证
源码解析
关键变量的理解HashMap源码中有很多的内部变量 , 这些变量会在下面源码分析中经常出现 , 首先需要理解这些变量的意义 。
扩容HashMap源码中把初始化操作也放到了扩容方法中 , 因而扩容方法源码主要分为两部分:确定新的数组大小、迁移数据 。 详细的源码分析如下 , 我打了非常详细的注释 , 可选择查看 。 扩容的步骤在上述已经讲过了 , 读者可以自行结合源码 , 分析HashMap是如何实现上述的设计 。
final Node<KV>[
resize() {
   // 变量分别是原数组、原数组大小、原阈值;新数组大小、新阈值
   Node<KV>[
oldTab = table;
   int oldCap = (oldTab == null) ? 0 : oldTab.length;
   int oldThr = threshold;
   int newCap newThr = 0;

   // 如果原数组长度大于0
   if (oldCap > 0) {
       // 如果已经超过了设置的最大长度(1<<30也就是最大整型正数)
       if (oldCap >= MAXIMUM_CAPACITY) {
           // 直接把阈值设置为最大正数
           threshold = Integer.MAX_VALUE;
           return oldTab;
       
       else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
           // 设置为原来的两倍
           newThr = oldThr << 1;
   

   // 原数组长度为0 , 但最大限度不是0 , 把长度设置为阈值
   // 对应的情况就是新建HashMap的时候指定了数组长度
   else if (oldThr > 0)
       newCap = oldThr;
   // 第一次初始化 , 默认16和0.75
   // 对应使用默认构造器新建HashMap对象
   else {              
       newCap = DEFAULT_INITIAL_CAPACITY;