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)
  • 数据结构

    • 数据结构之堆Heap
    • 数据结构之哈希表
    • 数据结构之队列
    • 数据结构之列表List
    • 数据结构之树Tree
    • 数据结构之图
    • 数据结构之栈Stack
    • 数据结构之Map
      • HashMap
        • 简介
        • 版本差异
        • 源码探究
        • 线程不安全可能引发的链表成环问题(JDk1.7)
      • HashTable
      • CurrentHashMap
        • 介绍
        • 区别
      • TreeMap
      • 参考
    • 数据结构之Set
  • 计算机网络

  • 算法

  • 操作系统

  • 基础
  • 数据结构
unclezs
2020-07-27
0
目录

数据结构之Map

# HashMap

# 简介

在Java中,HashMap是Map的最常用的。HashMap主要用来存放键值对,它基于哈希表的Map接口实现,是常用的Java集合之一。与HashTable主要区别为不支持同步和允许null作为key和value,所以如果你想要保证线程安全,可以使用ConcurrentHashMap代替而不是线程安全的HashTable,因为HashTable基本已经被淘汰

# 版本差异

# JDK1.8之前

JDK1.8之前,HashMap使用了拉链法解决冲突。也就是在Hash冲突的时候,直接将冲突的加入链表即可 缺陷:如果散列分布不均匀,导致大量不同数据都有同一个Hash地址,这样拉链法的这个链表就会很长了,如果过于长,会降低HashMap的性能。

# JDK1.8之后

为了解决当链表过长出现的性能问题,HashMap采用拉链法+红黑树的方式解决冲突。也就是首先使用拉链法的方式,当链表长度达到一定长度之后(默认为8),就将链表转换成红黑树。

# 源码探究

以下都来自JDK1.8

# 核心成员

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    // 序列号
    private static final long serialVersionUID = 362498820763181265L;    
    // 默认的初始容量是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;   
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30; 
    // 默认的填充因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当桶(bucket)上的结点数大于这个值时会转成红黑树
    static final int TREEIFY_THRESHOLD = 8; 
    // 当桶(bucket)上的结点数小于这个值时树转链表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 桶中结构转化为红黑树对应的table的最小大小
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存储元素的数组,总是2的幂次倍
    transient Node<k,v>[] table; 
    // 存放具体元素的集
    transient Set<map.entry<k,v>> entrySet;
    // 存放元素的个数,注意这个不等于数组的长度。
    transient int size;
    // 每次扩容和更改map结构的计数器
    transient int modCount;   
    // 临界值 当实际大小(容量*填充因子)超过临界值时,会进行扩容
    int threshold;
    // 装填因子
    final float loadFactor;
}
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
  1. 其中的MAXIMUM_CAPACITY = 1 << 30; 相当于是2的30次方。int一共32位,为什么不左移31位呢,也是2的幂次。原因就是因为31位为符号位,如果左移31位了就会是一个负数了,所以只能左移31位。
  2. loadFactor就是装填因子,具体可以查看哈希表的那篇博文,HashMap默认设置为0.75f。
  3. threshold,也就是容量阈值,就是装到了多少比例了再扩容,当作变量是不用每次都计算。只在容量变化时候计算。

# 树节点与链表节点

  1. 链表节点
static class Node<K,V> implements Map.Entry<K,V> {
     // 哈希值,存放元素到hashmap中时用来与其他元素hash值比较
     final int hash;
     final K key;
     V value;
     // 指向下一个节点
     Node<K,V> next;
}
1
2
3
4
5
6
7
8
  1. 红黑树节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;
        TreeNode<K,V> left; 
        TreeNode<K,V> right; 
        // needed to unlink next upon deletion
        TreeNode<K,V> prev;  
        // 是否为红色节点  
        boolean red;         
}
1
2
3
4
5
6
7
8
9

# 增删查方法

1.put与putVal

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
      Node<K,V>[] tab; Node<K,V> p; int n, i;
      //如果还没有初始化就进行初始化
      if ((tab = table) == null || (n = tab.length) == 0)
          n = (tab = resize()).length;
      //这里对hash值进行取余,保证hash的地址在数组长度以内,并且确定存放在哪个桶中,如果桶为空,则新的节点就放入桶中(在数组中的)
      if ((p = tab[i = (n - 1) & hash]) == null)
          tab[i] = newNode(hash, key, value, null);
      //证明此时桶不为空    
      else {
          Node<K,V> e; K k;
          //桶的第一个元素hash值和key值和新的节点一样
          if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
              e = p;
         //hash值不相等,即key不相等;
         //为红黑树结点 时候    
          else if (p instanceof TreeNode)
              e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
         //为链表结点 时候       
          else {
            //遍历到最后一个节点,将新的节点插入尾部
              for (int binCount = 0; ; ++binCount) {
                 //没有下一个节点了则到尾部了
                  if ((e = p.next) == null) {
                      p.next = newNode(hash, key, value, null);
                      //如果链表长度到达了阈值,则转化成红黑树
                      if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                          treeifyBin(tab, hash);
                      break;
                  }
                  //遍历到的链表中的这个节点hash值和key值和新的节点一样,发现存在了,跳出循环。当前e为这个重复的节点。p为上一个节点
                  if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                      break;
                  //切换父节点为当前节点,做遍历
                  p = e;
              }
          }
          //e的值存在
          if (e != null) { // existing mapping for key
              V oldValue = e.value;
              //只有在值不存(onlyIfAbsent=false)在的或者为Null的时候才更新
              if (!onlyIfAbsent || oldValue == null)
                  e.value = value;
              // 访问后回调    
              afterNodeAccess(e);
              return oldValue;
          }
      }
      ++modCount;
      //判断是否需要扩容
      if (++size > threshold)
          resize();
      // 插入后回调    
      afterNodeInsertion(evict);
      return null;
  }
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

可以看到put方法只是对key进行了hash,然后调用了putVal方法,核心的添加逻辑还是在putVal方法里面。 因为putTreeVal有添加树节点和红黑树的转换,不是这里的重点,具体可以查看红黑树那篇。

  1. remove与removeNode
  public V remove(Object key) {
          Node<K,V> e;
          return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
  }
  final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {
          Node<K,V>[] tab; Node<K,V> p; int n, index;
          //hash表已经初始化,并且这个位置有节点,才进入逻辑
          if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1) & hash]) != null) {
              Node<K,V> node = null, e; K k; V v;
              //桶中第一个元素key和hash值与新来的key相等
              if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                  node = p;
              //不相等的时候,如果存下一个节点    
              else if ((e = p.next) != null) {
                  //桶中第一个节点节点为树节点时,拿到这个要删除的节点
                  if (p instanceof TreeNode)
                      node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                  //为链表时    
                  else {
                      //遍历链表,直到找到
                      do {
                          if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) {
                              node = e;
                              break;
                          }
                          p = e;
                      } while ((e = e.next) != null);
                  }
              }
              //这个key对应的节点存在,并且不匹配值,或者值与期望的值相等,则进行删除。
              if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {
                  //如果是树节点,就调用删除树节点的方法
                  if (node instanceof TreeNode)
                      ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                  //如果是第一个节点,那就把节点前移   ,桶中第一个节点变为下一个节点 
                  else if (node == p)
                      tab[index] = node.next;
                  //否则 直接把节点前移  
                  else
                      p.next = node.next;
                  ++modCount;
                  --size;
                  //一处后的回调
                  afterNodeRemoval(node);
                  return node;
              }
          }
          //没有找到这个key则直接返回null
          return null;
  }
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
  1. get与getNode
public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
      Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
      //hash表已经初始化,并且这个位置有节点,才进入逻辑
      if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
          //如果桶中第一个就是了,直接返回
          if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
              return first;
          //如果存在下一个节点    
          if ((e = first.next) != null) {
              //桶中第一个如果是树节点。直接查找树
              if (first instanceof TreeNode)
                  return ((TreeNode<K,V>)first).getTreeNode(hash, key);
              //不是的话就遍历链表,知道找到为止    
              do {
                  if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
                      return e;
              } while ((e = e.next) != null);
          }
      }
      //没有找到返回null
      return null;
  }
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

# hash表扩容

当hashmap中的元素越来越多的时候,碰撞的几率也就越来越高(因为数组的长度是固定的),所以为了提高查询的效率,就要对hashmap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,所以这是一个通用的操作,很多人对它的性能表示过怀疑,不过想想我们的“均摊”原理,就释然了,而在hashmap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

扩容条件就是当threshold=loadFactory*capacity大于等于hash表当前的节点个数

    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

对于以上的为链表时候的扩容源码解读,因为一两句说不清楚,推荐阅读深入理解HashMap(四): 关键源码逐行分析之resize扩容 (opens new window)

# 线程不安全可能引发的链表成环问题(JDk1.7)

推荐阅读大多数人不知道的:HashMap链表成环的原因和解决方案 (opens new window)

# HashTable

HashTable和HashMap的实现原理几乎一样, 差别无非是

  1. HashTable不允许key和value为null;
  2. HashTable是线程安全的。

但是HashTable线程安全的策略实现代价却太大了,简单粗暴,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。

# CurrentHashMap

# 介绍

  1. JDK1.7 HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想。

  2. JDK1.8 取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,并发控制使用Synchronized和CAS来操作

# 区别

从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树,总结如下:

  1. JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8实现降低锁的粒度就是HashEntry(首节点)
  2. JDK1.8版本的数据结构变得更加简单,去掉了Segment这种数据结构,使用synchronized来进行同步锁粒度降低,所以不需要分段锁的概念,实现的复杂度也增加了
  3. JDK1.8使用红黑树来优化链表,基于长度很长的链表的遍历是一个很漫长的过程,而红黑树的遍历效率是很快的,代替一定阈值的链表,这样形成一个最佳拍档
  4. JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock:
    • 低粒度加锁方式,synchronized并不比ReentrantLock差,粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
    • JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
    • 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存

# TreeMap

TreeMap底层为红黑树,线程不安全的,如果需要使用请使用线程安全的,可以使用


Collections.synchronizedSortedMap();
Collections.synchronizedMap(new TreeMap());

1
2
3
4

# 参考

  1. 集合框架源码学习之HashMap(JDK1.8) (opens new window)
  2. ConcurrentHashMap实现原理及源码分析 (opens new window)
在 GitHub 编辑此页 (opens new window)
上次更新: 2024/02/25, 12:11:11
数据结构之栈Stack
数据结构之Set

← 数据结构之栈Stack 数据结构之Set→

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