Java类集---HasMap源码分析(resize和树化)HashtableTreeMap ConcurrentHashMap

Map集合一次性会保存两个对象,即键值对。
Map集合:

public interface Map<K,V>

Key值唯一,通过一个key值能唯一找到一个vaule值。
Map接口的核心方法:

public V put(K key,V value) :向Map中添加数据
public V get (K key) :根据指定的key取得相应的vaule值,若没有此值,返回null
public Set<Map.Entry<K,V>> entrySet():将Map集合变为Set集合
public Set<K> keySet():返回所有Key值集合,key不能重复
public Collection<V> values( ):返回所有的vaule值,value可以重复

代码如下:

 public static void main(String[] args) {
        HashMap<Integer, String> map = new HashMap<>();
        map.put(2, "sophia");
        map.put(1, "pick");
        map.put(null, null);
        map.put(null, "happy");
        System.out.println(map); //{null=happy, 1=pick, 2=sophia}

        //通过key获取value
        System.out.println("key=1 value=" + map.get(1));
        System.out.println("key=null value=" + map.get(null)); //不允许重复,将值覆盖

        //获取key
        Set<Integer> set = map.keySet();
        System.out.println("所有的key:" + set); //所有的key:[null, 1, 2]

        //获取value
        Collection<String> list = map.values();
        System.out.println("所有的value"+list); //所有的value[happy, pick, sophia]

        //遍历map
        Set<Map.Entry<Integer, String>> entrySet = map.entrySet();
        //方法一:取得entrySet的迭代器
        Iterator<Map.Entry<Integer,String>> iterator=entrySet.iterator();
        while(iterator.hasNext())
        {
            Map.Entry<Integer,String> entry=iterator.next();
            System.out.println(entry.getValue()+"="+entry.getKey()); //happy=null  pick=1  sophia=2
        }
        //方法2:for-each输出
        for (Map.Entry<Integer, String> entry : entrySet)
        {
            System.out.println(entry.getKey()+"->"+entry.getValue()); //null->happy   1->pick  2->sophia
        }
       
          //方法3:for循环输出
        for(Integer key:map.keySet())
        {
            System.out.println(key+"="+map.get(key));  //null=happy  1=pick 2=sophia
        }
    }

Map集合有如下四个常用子类
HashMap TreeMap HashTable ConcurrentHashMap
1. HashMap
HashMap是Map最常用子类。

  • key可以为NULL;
  • value可以为NULL
  • key不可以重复,如果重复相当于覆盖
  • 线程不安全(不同步)
  • 在JDK8以前,底层实现是哈希表,JDK之后,底层是哈希表和红黑树。

HashMap源码分析:
HashMap的内部结构是数组(Node[ ] table)和链表组合而成的复合结构,数组被分为一个个桶,通过哈希值决定了键值对在这个数组的寻址,哈希值相同的键值对,则以链表形式存储。但是当达到一定条件后,会树化(红黑树)。
一:成员变量

DEFAULT_INITIAL_CAPACITY = 1 << 4;  桶的个数为162的次方)
MAXIMUM_CAPACITY = 1 << 30   桶的最大个数
DEFAULT_LOAD_FACTOR = 0.75f;   负载因子
TREEIFY_THRESHOLD = 8 (树化阈值,链表元素个数为8)
MIN_TREEIFY_CAPACITY = 64  哈希表元素个数(树化要求的最少哈希表元素个数)
UNTREEIFY_THRESHOLD= 6;  链表元素个数为6在resize阶段,解除树化阈值

二:初始化策略(懒加载策略,在第一次put初始化哈希表)

public HashMap() {
		//只是初始化负载因子
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

问题:要求哈希表初始化容量必须为2^n,若通过构造方法传入一个非2^n数值,
hashmap会在内部调用tableSizeFor返回一个距离最近的2^n数值,
传15返回16,传3132 100-<128

在HashMap的构造方法中,只有对负载因子做了初始化,并没有初始化数组和链表。
三:put( )
当给HashMap添加元素时

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

首先将key值进行哈希计算:

//哈希算法:返回高低16位共同参与运算的一个hash值
static final int hash(Object key) {
        int h;
        //当key为null时,直接返回0,也就是在第一个桶
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

计算出key的哈希码后再和右移16位的值进行异或运算。
在这里插入图片描述

右移16位,正好是32bit一半,自己的高半区和低半区做异或,是为了混合原始哈希码的高低位,以此来加大低位的随机性。而且混合后的低位掺杂了高位的部分特征,这样高位的信息被变相保存起来。避免了哈希碰撞。(n-1)&hash保证了hash值在数组长度范围之内。
为什么不直接采用Object提供的hashcode方法?
hashcode返回的是int值,即32位,那么几乎不会发生碰撞,桶的个数就很多,就没有意义。

 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //此时哈希表还未被初始化,利用resize()初始化
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
            //如果哈希码所在位置为null,即没有元素,将该元素直接插入在位置
            //当前桶中没有元素,p指向当前桶的头结点
        if ((p = tab[i = (n - 1) & hash]) == null)
        	//将要保存的结点作为桶的头结点保存
            tab[i] = newNode(hash, key, value, null);
        else {
            //此时哈希表已经初始化并且分的桶不为空,有头结点
            Node<K,V> e; K k;
            //如果要保存的结点和头结点(p)key值一样,直接替换头结点(包括头结点是null,也替换)
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
                //若桶中元素已经树化,按照树的方式来存储新节点
            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);
                        //如果链表元素个数大于8  TREEIFY_THRESHOLD=8  尝试调用树化方法将链表树化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //链表中存在相同key,替换其value
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //替换结点值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        //添加结点后,整个HashMap元素个数容量超过threshold(哈希表桶个数*负载因子),将会扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }


//树化
final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        //当哈希表所有元素个数小于MIN_TREEIFY_CAPACITY(64)时,只是简单的扩容
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            //这个过程是将所有的Node结点转换为TreeNode结点并做好连接准备
            if ((tab[index] = hd) != null)
                hd.treeify(tab); //将链表转化为红黑树
        }
    }

在put元素是计算元素的存放位置时,利用的是p = tab[i = (n - 1) & hash]),(n - 1) & hash相当于是hash%(n-1),但是用二进制位运算速度较快,十进制还需要先转换为二进制再计算。

put流程:

  • 当哈希表没有初始化,会调用resize( )进行初始化操作

  • 对key值hash取得要存储的桶下标
    1.若该桶为空,直接将该结点作为桶的头结点
    2. 若桶不为空
    a. 该节点和桶的头结点key值相同,直接替换为头结点(包括头结点是null)
    b.若树化,使用树的方式添加 新结点
    c.没有树化,将新节点尾插到链表后,添加元素后,链表的元素个数binCount>=树化阈值-1,尝试进行树化操作
    3.上面操作,如果存在key值相同节点,直接将value替换

  • 添加元素后,计算整个哈希表大小,若超过threshold(哈希表桶个数 * 负载因子),进行resize扩容操作(比如所有元素个数超过12=16*0.75扩容)

当桶中链表元素个数超过8,并且哈希表的所有元素个数超过64,此时会将此桶中链表转为红黑树结构,否则(只是桶中链表元素个数超多8),只是简单的扩容操作。
树化不是整表树化,将该树化的链表树化

get( )

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;
        //哈希表已经初始化,并且分的桶的头结点不为空,即该桶有元素
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //要查找结点的key和头结点key相同,直接返回头结点
            if (first.hash == hash && // always check first node
                ((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;
    }


get 流程

  • 哈希表为空或者分的桶的头结点为空(桶中还没有元素),直接返回null
  • 哈希表已经初始化并且分的桶头结点不为空
    1.查找结点的key值等于头结点,返回头结点
    2.进行桶的遍历,查找指定结点
    a.若桶已经树化,按照树的方式进行查找
    b.按照链表查找

扩容机制

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) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //将原数组扩容为2倍
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                     //阈值*2
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
        //如果数组为空,将数组初始化为长度为DEFAULT_INITIAL_CAPACITY=16,阈值为负载因子*16=12
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        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;
    }
    ...  ...
  }

为什么初始化容量为2^N?
可以提高速度。。
扩容流程resize( )

  • 判断哈希表是否初始化,若还未初始化,根据InittalCapacilty值进行初始化操作
  • 若表已经初始化,将原哈希表按照2倍方式扩容
  • 扩容后进行原表元素的移动
    1.若桶中元素已经树化,调用树的方式移动元素(若在移动过程中发现红黑树的结点<=6,会将红黑树进行解除树化,还原为链表)
    2.若还未树化,调用链表的方式移动元素

HashMap存在的性能问题

  • 多线程场景下,由于条件竞争,很容易造成死锁(使用ConcurrentHashMap)
  • rehash是一个比较耗时的过程(在能预估存储元素的个数,尽量自定义初始化容量:2^n次方,尽量减少rehash过程)

容量和负载因子
容量和负载因子决定了可用的桶的数量,空桶太多会浪费空间,如果使用的太满则会严重影响操作的性能。极端情况下,假设只有一个桶,那么它就退化成了链表,完全不能提供所谓常数时间存的性能。
所以要合理处理容量和负载因子值:
负载因子*容量>元素数量
红黑树:
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。 [注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!]
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
基于以上特征:新加入的节点总是红色的:
因为被插入前的树结构是构建好的,一但我们进行添加黑色的节点,无论添加在哪里都会破坏原有路径上的黑色节点的数量平等关系,所以插入红色节点是正确的选择。
红黑树的优势:
比AVL树相比优点是不用在节点类中保存一个节点高度这个变量,节省了内存。而且红黑树一般不是以递归方式实现的而是以循环的形式实现。
一般的操作在最坏情形下花费O(logN)时间。
为什么要树化?
本质上这是个安全问题。因为在元素放置过程中,如果一个对象哈希冲突,都被放置到同一个桶里,则会形成一个链表,我们知道链表查询是线性的,会严重影响存取的性能。链表查找是O(n),二叉树查找是log(N) ,查找速度快。当桶中链表长度太长,会大大影响查找速度,因此将其树化来提高查找结点的速度。
而在现实世界,构造哈希冲突的数据并不是非常复杂的事情,恶意代码就可以利用这些数据大量与服务器端交互,导致服务器端 CPU 大量占用,这就构成了哈希碰撞拒绝服务攻击。
为什么链表长度大于等于8才树化?
基于时间复杂度是O(logN)。
当长度为8,平均查找时间是3,而链表是8/2=4,就有转化为数的必要,而链表长度在8以内,转化为树还有生成树的时间,并不可取。
HashSet和HashMap判断元素是否重复:
先调用hashCode计算出对象hash码决定存放的数据桶
而后使用quals来比较元素是否相等,若相等,则不再放置元素,若equals返回false,则在相同桶之后,使用链表将若干元素链起来。
object类提供的hashCode方法默认使用对象的地址进行hash

//覆写hashCode
public int hashCode()
{
    return Objects.hash(age,name);  Objects:所有类父类提供的若干方法 
     //当两个元素的属性相等,则一定放在同一个桶中
}

总结:

  1. HashMap是延迟初始化,在有put时初始化,将哈希表初始化为大小为16,哈希表所有元素阈值为12;
  2. 树化:当链表长度大于8和数组长度大于64进行树化为红黑树,如果数组长度小于64,则是扩容,并且对桶的元素重新进行哈希运算。

Hashtable

  • Hashtable的key不能为NULL
  • Hashtable的value不能为NULL
  • Hatable是线程安全的类(在多线程情况下使用,加锁)
  • JDK1.0产生
  • 底层是哈希表+链表

Hashtable:基于synchronized存储元素
线程安全:在put/get/remove等方法使用内建锁将整个哈希表上锁
使用串行操作,性能很差,比如 在第一个桶中get元素,不能在其他桶干其他事情
初始化策略:当产生Hashtable对象时就将哈希表初始化,并且初始化默认是11,并不会将11改为2^N


如何优化Hashtable性能?
将大锁拆分成小锁,将锁细粒度化,将整张表的锁分段化,由此有了ConcurrentMap

HashMap可以转化为再多线程下使用:

public static void main(String[] args){
        //线程不安全的Map
        Map<Integer,String> hashmap=new HashMap<>();
        //线程安全的Map
        Map<Integer,String> map=Collections.synchronizedMap(hashmap);
        System.out.println(map.getClass().getName());//java.util.Collections$SynchronizedMap
    }

在这里插入图片描述
ConcurrentHashMap
为什么要有ConcurrentHashMap?
Hashtable 本身比较低效,因为它的实现基本就是将 put、get、size 等各种方法加上“synchronized”。简单来说,这就
导致了所有并发操作都要竞争同一把锁,一个线程在进行同步操作时,其他线程只能等待,大大降低了并发操作的效率。

ConcurrentHashMap 分析:
早期的ConcurrentHashMap(JDK1.7):基于分段锁Segment来实现,每个Segment都是ReentrantLock的子类

  • 结构:将哈希表拆分成16个Segment,每个Segment下有一个小的哈希表
  • 关于锁:将原先整张表的一把锁细粒度化每个Segment一把锁(16把锁),并且不同Segment之间互不干扰,每个Segment实际上ReentrantLock
  • 扩容机制:Segment初始化后无法扩容(默认初始化为16),扩容实际上每个Segment下的小哈希表,并且不同的Segment之间扩容互不影响

后期的ConcurrentHashMap(JDK8):使用CAS+同步代码块(synchronized)

  • 结构上:与JDK1.8的HashMap基本一样,是哈希表+红黑树的底层结构
  • 关于锁:将锁先锁一片区域再次细粒度化为只锁桶中的头结点
  • 原先的Segment保留但是没有实际含义,仅仅用作序列化,数据存储利用volatile 来保证可见性。

对比JDK1.7和JDK8的ConcurrentHash:

  • 结构上: JDK1.7 基于分段锁的16个Segment,每个Segment下是一个小哈希表,而JDK8是哈希表+红黑树
  • 锁的使用:JDK1.7是使用ReentrantLock将不同的Segment上锁,JDK8是以CAS和synchronized给每个桶的头结点上锁,再次将锁细粒度化

为什么JDK8重新使用synchronized内建锁?

  1. synchronized在JDK1.5后不断优化,在现版本的JDK中,synchronized与lock的性能上基本差不多,甚至在地竞争场景下还会优于lock
    2.使用synchronized可以节省大量内存空间,这是相较于ReentrantLock最大的优势,因为ReentrantLock会将所有元素入同步队列竞争锁,是一个小内存开销,即使是单线程,也会有内存开销。

TreeMap

TreeMap可以排序,
TreeMap类似于TreeSet,如果是自定义类,需要实现Comparable或者实现外置Comparator接口。

import java.util.Comparator;
import java.util.TreeMap;

class Person2
{
    private String name;
    private int age;

    public Person2(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

//降序排
//降序:<  return 1
//    = return 0
//   > return -1
class DescSort implements Comparator<Person2>
{
    @Override
    public int compare(Person2 o1, Person2 o2) {
        if(o1.getAge()>o2.getAge())
            return -1;
        else if(o1.getAge()==o2.getAge())
            return 0;
        return 1;
    }
}
public class TestTreeMap {
    public static void main(String[] args) {
        TreeMap<Person2,Integer> treeMap=new TreeMap<>(new DescSort());
        treeMap.put(new Person2("pick",18),1);
        treeMap.put(new Person2("sophia",20),2);
        treeMap.put(new Person2("pick",18),3);
        System.out.println(treeMap); //{Person{name='sophia', age=20}=2, Person{name='pick', age=18}=3}

    }
}

利用TreeMap排序:

package CODE.Map1;


import CODE.JavaIo.In;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.*;

//以TreeMap将一个文件给定的学号 姓名 按学号降序输出
public class FileTreeMap {
    public static void main(String[] args) {
        File file=new File("C:"+File.separator+"Users"+
                File.separator+ "lenovo"+File.separator+"Desktop"+
                File.separator+"Test.txt");
        //实现Comparator接口,匿名内部类
        Map<Integer,String> treeMap=new TreeMap<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2)
            {
                //降序
                if(o1>o2)
                    return -1;
                else if(o1<o2)
                    return 1;
                return 0;
                //或者return o1.compareTo(o2)*-1;以为Integer类comparaTo()有,可以直接复用
            }
        });
        try {
            Scanner scanner=new Scanner(new FileReader(file));
            while(scanner.hasNext())
            {
                String[] str=scanner.nextLine().split("\t");
                treeMap.put(Integer.parseInt(str[0]),str[1]);
            }
            Set<Map.Entry<Integer,String>> entrySet=treeMap.entrySet();
            Iterator<Map.Entry<Integer,String>> iterator=entrySet.iterator();
            while(iterator.hasNext())
            {
                Map.Entry<Integer,String> entry=iterator.next();
                System.out.println(entry.getKey()+"  "+entry.getValue());
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}

猜你喜欢

转载自blog.csdn.net/sophia__yu/article/details/86563581