Exploring
首页
  • Java

    • 面向对象的思想OOP
    • 浅谈Java反射原理
    • endorsed覆盖JDK中的类
  • 认证与授权

    • LDAP概念和原理介绍
    • OAuth2介绍
  • Impala

    • Impala 介绍
  • MySQL

    • 关于MySQL的一些面试题
    • 解决MySQL不到中文数据
    • 数据库之事务与实现原理
  • Oracle

    • oracle的表空间,用户管理,表操作,函数
    • oracle的查询、视图、索引
    • plsql简单入门
  • Redis

    • 数据类型详解
    • 跳越表
    • 数据持久化的两种方式
  • 共识算法

    • gossip
  • RPC

    • GRPC初识与快速入门
    • ProtocolBuffer基本语法
  • RabbitMQ

    • RabbitMQ入门程序之HelloWorld
    • RabbitMQ之工作模式
  • Zookeeper

    • Zookeeper一文入门
  • Docker

    • Docker入门初体验
  • Maven

    • 把自己的包到Maven中央仓库
    • Maven之自定义插件
  • Nginx

    • nginx的安装
    • nginx的配置文件
    • nignx 的变量
  • Tomcat

    • Servlet3通过SPI进行注册组件
  • Vagrant

    • vagrant 初始化
    • vagrant 常用配置
    • vagrant 自己制作 box
  • Linux

    • 启动方式 Systemd
    • 后台服务
    • 防火墙与 Iptables
  • 设计模式

    • 设计模式-代理
    • 设计模式-单例模式
    • 设计模式-迭代器
  • 分布式

    • CAP 理论
  • 数据结构

    • 数据结构之堆Heap
    • 数据结构之哈希表
    • 数据结构之队列
  • 计算机网络

    • HTTP与HTTPS详解
    • 浅谈DNS协议
    • ISP中的网络层
  • 算法

    • 常用查找算法及Java实现
    • 常用排序算法及Java实现
    • 迪杰斯特拉算法
  • 操作系统

    • 操作系统之进程调度算法
    • 操作系统之进程通讯IPC
    • 操作系统之内存管理
  • 抓包

    • 生成安卓系统证书
  • 加解密

    • 常见加密算法
    • 公开秘钥基础知识
    • RSA 解析
  • Windows

    • scoop 包管理
    • windows-terminal 配置
    • 增强 PowerShell
归档
Github (opens new window)
首页
  • Java

    • 面向对象的思想OOP
    • 浅谈Java反射原理
    • endorsed覆盖JDK中的类
  • 认证与授权

    • LDAP概念和原理介绍
    • OAuth2介绍
  • Impala

    • Impala 介绍
  • MySQL

    • 关于MySQL的一些面试题
    • 解决MySQL不到中文数据
    • 数据库之事务与实现原理
  • Oracle

    • oracle的表空间,用户管理,表操作,函数
    • oracle的查询、视图、索引
    • plsql简单入门
  • Redis

    • 数据类型详解
    • 跳越表
    • 数据持久化的两种方式
  • 共识算法

    • gossip
  • RPC

    • GRPC初识与快速入门
    • ProtocolBuffer基本语法
  • RabbitMQ

    • RabbitMQ入门程序之HelloWorld
    • RabbitMQ之工作模式
  • Zookeeper

    • Zookeeper一文入门
  • Docker

    • Docker入门初体验
  • Maven

    • 把自己的包到Maven中央仓库
    • Maven之自定义插件
  • Nginx

    • nginx的安装
    • nginx的配置文件
    • nignx 的变量
  • Tomcat

    • Servlet3通过SPI进行注册组件
  • Vagrant

    • vagrant 初始化
    • vagrant 常用配置
    • vagrant 自己制作 box
  • Linux

    • 启动方式 Systemd
    • 后台服务
    • 防火墙与 Iptables
  • 设计模式

    • 设计模式-代理
    • 设计模式-单例模式
    • 设计模式-迭代器
  • 分布式

    • CAP 理论
  • 数据结构

    • 数据结构之堆Heap
    • 数据结构之哈希表
    • 数据结构之队列
  • 计算机网络

    • HTTP与HTTPS详解
    • 浅谈DNS协议
    • ISP中的网络层
  • 算法

    • 常用查找算法及Java实现
    • 常用排序算法及Java实现
    • 迪杰斯特拉算法
  • 操作系统

    • 操作系统之进程调度算法
    • 操作系统之进程通讯IPC
    • 操作系统之内存管理
  • 抓包

    • 生成安卓系统证书
  • 加解密

    • 常见加密算法
    • 公开秘钥基础知识
    • RSA 解析
  • Windows

    • scoop 包管理
    • windows-terminal 配置
    • 增强 PowerShell
归档
Github (opens new window)
  • Java

    • 基础

      • 面向对象的思想OOP
      • 浅谈Java反射原理
      • endorsed覆盖JDK中的类
      • Java8新特性之函数式接口
      • Java集合之HashMap与ConcurrentHashMap
        • HashMap
          • 介绍
          • 添加元素的时间复杂度
          • 扩容机制
          • JDK7并发情况的成环问题
          • hashCode方法
          • 转化为红黑树及退化成链表原因
        • ConcurrentHashMap
      • Java语法级常见面试题
      • Java中的四种引用类型详解
      • Java中的摘要算法MessageDigest
      • Java中注解Annotation概念及原理
      • Java中final关键词详解
      • jdk源码分析-TreeMap红黑树插入删除过程
    • 并发与多线程

    • 日志系统

    • 单元测试

    • JVM

    • Spring

    • SpringBoot

    • 一些工具

  • 语言
  • Java
  • 基础
unclezs
2018-05-15
0
目录

Java集合之HashMap与ConcurrentHashMap

# HashMap

# 介绍

在JDK1.7时,底层是由数组+链表与拉链法解决冲突的。

在JDK1.8时,底层是由数组+链表与拉链法+链表长度为8时转为红黑树。但是红黑树节点少于6时退化为链表。

# 添加元素的时间复杂度

put操作的流程:

第一步:key.hashcode(),时间复杂度O(1)。

第二步:找到桶以后,判断桶里是否有元素,如果没有,直接new一个entey节点插入到数组中。时间复杂度O(1)。

第三步:如果桶里有元素,并且元素个数小于6,则调用equals方法,比较是否存在相同名字的key,不存在则new一个entry插入都链表尾部。时间复杂度O(1)+O(n)=O(n)。

第四步:如果桶里有元素,并且元素个数大于6,则调用equals方法,比较是否存在相同名字的key,不存在则new一个entry插入都链表尾部。时间复杂度O(1)+O(logn)=O(logn)。红黑树查询的时间复杂度是logn。

通过上面的分析,我们可以得出结论,HashMap新增元素的时间复杂度是不固定的,可能的值有O(1)、O(logn)、O(n)。最好情况是O(1),最坏情况是O(n)

# 扩容机制

扩容条件就是当threshold=loadFactory*capacity大于等于hash表当前的节点个数,HashMap采用2倍扩容。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    //旧的容量
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //旧的阈值
    int oldThr = threshold;
    //新的容量与阈值
    int newCap, newThr = 0;
    //如果初始化过了
    if (oldCap > 0) {
      //如果旧的容量大于了最大的容量 2的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; // double threshold
    }
    //未初始化的时候
    //初始阈值大于0则将新的容量设置为旧的阈值
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    //否则使用默认的容量进行初始化新的容量    
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        //计算新的阈值
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //计算新的阈值,如果新的阈值为0的话
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    //更新新的阈值
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
       //遍历每个桶,拷贝到新的hash数组
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //如果桶不为空
            if ((e = oldTab[j]) != null) {
                //释放以前节点的空间
                oldTab[j] = null;
                //这个桶只有这一个节点
                if (e.next == null)
                   //直接把这个节点放到新的桶里
                    newTab[e.hash & (newCap - 1)] = e;
                //不只有一个节点
                //是树节点的时候    
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //为链表的时候    
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    //遍历桶的所有节点
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);

                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

# JDK7并发情况的成环问题

在 JDK7 版本下,很多人都知道 HashMap 会有链表成环的问题,但大多数人只知道,是多线程引起的,至于具体细节的原因,和 JDK8 中如何解决这个问题,很少有人说的清楚,百度也几乎看不懂,本文就和大家聊清楚两个问题:

  1. JDK7 中 HashMap 成环原因,2
  2. JDK8 中是如何解决的。

成环核心原因在JDK7的transfer方法。

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            //e为空时循环结束
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                // 成环的代码主要是在这三行代码
                // 首先插入是从头开始插入的
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

JDK8中的扩容方法,重新写了,为resize,链表扩容的核心代码:

    // loHead 表示老值,老值的意思是扩容后,该链表中计算出索引位置不变的元素
    // hiHead 表示新值,新值的意思是扩容后,计算出索引位置发生变化的元素
    // 举个例子,数组大小是 8 ,在数组索引位置是 1 的地方挂着一个链表,链表有两个值,两个值的 hashcode 分别是是9和33。
    // 当数组发生扩容时,新数组的大小是 16,此时 hashcode 是 33 的值计算出来的数组索引位置仍然是 1,我们称为老值
    // hashcode 是 9 的值计算出来的数组索引位置是 9,就发生了变化,我们称为新值。
    Node<K,V> loHead = null, loTail = null;
    Node<K,V> hiHead = null, hiTail = null;
    Node<K,V> next;
    do {
        next = e.next;
        // (e.hash & oldCap) == 0 表示老值链表
        if ((e.hash & oldCap) == 0) {
            if (loTail == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
        }
        // (e.hash & oldCap) != 0 表示新值链表
        else {
            if (hiTail == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
        }
    } while ((e = next) != null);
    // 老值链表赋值给原来的数组索引位置
    if (loTail != null) {
        loTail.next = null;
        newTab[j] = loHead;
    }
    // 新值链表赋值到新的数组索引位置
    if (hiTail != null) {
        hiTail.next = null;
        newTab[j + oldCap] = hiHead;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

总的来说,就是将每个桶的节点的hash值遍历出来,如果hash与oldCap取模为0,则表示数据不需要移动桶位置,如果不为0,则在桶的位置为 当前桶的索引+oldCap,举例解释:

oldCap=16,新的容量2倍扩容为32,遍历每个桶,当两个hashcode分别为5和21,在容量为16的时候,都在索引为5的桶,扩容为32时,相当于高位补了1,所以如果hashcode与olcCap取模后不再是以前的位置了则代表新位置为oldcap+oldIndex

总结

JDK7 是在 while 循环里面,单个计算好数组索引位置后,单个的进行头插法插入数组中,在多线程情况下,会有成环问题

JDK8 ,改用尾插法,是等链表整个 while 循环结束后,才给数组赋值,所以多线程情况下,也不会成环

推荐阅读:HashMap链表成环的原因和解决方案 (opens new window)

# hashCode方法

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
1
2
3
4

可以看到把hashcode的高16位与低16位进行了异或运算,这样做的好处就是让低16位中包含了高16位的特征,尽量避免了hash冲突。

之所以只取16位还有一个原因,那就是因为最终取模运算使用的是 (n-1)& hashcode方式代替%,之所以能够代替是因为n为2^x方。所以在容量比较少的时候,都是取的低16位的结果。

# 转化为红黑树及退化成链表原因

# 链表长度为8转化为红黑树原因

HashMap源码中可以看到:

这个其实就是泊松分布,节点的分布频率会遵循泊松分布,链表长度达到8个元素的概率为0.00000006,几乎是不可能事件。

还有就是红黑树的平均查找长度为O(logn)而链表为O(n)

# 节点个数少于6退化链表原因

  • 为什么转化为红黑树的阈值8和转化为链表的阈值6不一样,是为了避免频繁来回转化。
  • 退化是为了节省空间,红黑树占用的空间是链表的两倍。

首先得注意,网上说的到小于6退化为链表是分时机的,在移除节点的时候并不会判断小于6进行退化成链表,这个退化过程发生在resize中,为树节点的时候调用的spilt方法中进行的。

在remove的时候,通过红黑树根节点及其子节点是否为空来判断是否需要退化。

 final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,boolean  movable) {
      //只贴核心代码
      if (root == null || (movable && (root.right == null || (rl = root.left) == null|| rl.left == null))) {
                tab[index] = first.untreeify(map);  // too small
                return;
            }
 }
1
2
3
4
5
6
7

resize的时候,对红黑树进行了拆分

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
      TreeNode<K,V> b = this;
      // Relink into lo and hi lists, preserving order
      TreeNode<K,V> loHead = null, loTail = null;
      TreeNode<K,V> hiHead = null, hiTail = null;
      int lc = 0, hc = 0;
      for (TreeNode<K,V> e = b, next; e != null; e = next) {
          next = (TreeNode<K,V>)e.next;
          e.next = null;
          if ((e.hash & bit) == 0) {
              if ((e.prev = loTail) == null)
                  loHead = e;
              else
                  loTail.next = e;
              loTail = e;
              ++lc;
          }
          else {
              if ((e.prev = hiTail) == null)
                  hiHead = e;
              else
                  hiTail.next = e;
              hiTail = e;
              ++hc;
          }
      }
      //在这之前的逻辑是将红黑树每个节点的hash和一个bit进行&运算,
      //根据运算结果将树划分为两棵红黑树,lc表示其中一棵树的节点数
      if (loHead != null) {
          if (lc <= UNTREEIFY_THRESHOLD)
              tab[index] = loHead.untreeify(map);
          else {
              tab[index] = loHead;
              if (hiHead != null) // (else is already treeified)
                  loHead.treeify(tab);
          }
      }
      if (hiHead != null) {
          if (hc <= UNTREEIFY_THRESHOLD)
              tab[index + bit] = hiHead.untreeify(map);
          else {
              tab[index + bit] = hiHead;
              if (loHead != null)
                  hiHead.treeify(tab);
          }
      }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

这里才用到了 UNTREEIFY_THRESHOLD 的判断,当红黑树节点元素小于等于6时,才调用untreeify方法转换回链表

# ConcurrentHashMap

JDK7底层实现线程线程安全是通过分段锁Segment,继承自ReentrantLock。 JDK8底层改为CAS+Synchronized实现。

首先使用添加一个元素的时候,发现这个元素对应的Buket是空的,则通过CAS进行添加Buket的第一个元素。 如果元素的对应的Buket的不为空则通过同步代码块进行并发同步控制。

升级原因:

  • 减少内存开销:如果使用ReentrantLock则需要节点继承AQS来获得同步支持,增加内存开销,而1.8中只有头节点需要进行同步。
  • synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。
在 GitHub 编辑此页 (opens new window)
上次更新: 2024/02/25, 12:11:11
Java8新特性之函数式接口
Java语法级常见面试题

← Java8新特性之函数式接口 Java语法级常见面试题→

Theme by Vdoing | Copyright © 2018-2024 unclezs
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式