Java篇:树和Map

Java篇:树和Map

每次涉及到集合就想将Map拎出来单独看看,每次开始了解又似乎觉得没必要,而每次想到相关问题又只有隐隐约约的印象。

而提到Map就会想到TreeMap,就会想到红黑树。有关于树的概念我也总是这个状态,所以一起拎出来看看总结下加深印象。概念部分皆参考自列在参考链接中的博文。

1、数据结构:树

树的部分主要参考:数据结构之树

1.1 树

作为计算机中常用的数据机构--树(Tree),随着在计算机中应用越发广泛,衍生出了许多结构:树、二叉树、二叉查找树、平衡二叉树(AVL树)、红黑树、哈夫曼树(Huffman Tree)、多路查找树、B树、B+树、B*树、R树

在计算机科学中,树(英语:tree)是一种抽象数据类型或是实现这种抽象数据类型的数据结构,用来模拟具有树状结构性质的数据集合。它是由n(n>0)个有限节点组成一个具有层次关系的集合。把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:

  • ①每个节点有零个或多个子节点;

  • ②没有父节点的节点称为根节点;

  • ③每一个非根节点有且只有一个父节点;

  • ④除了根节点外,每个子节点可以分为多个不相交的子树;

 

然后你要知道一大堆关于树的术语:度,叶子节点,根节点,父节点,子节点,深度,高度。

1.2 二叉树

1.2.1 二叉树

二叉树:每个节点最多含有两个子树的树称为二叉树。(我们一般在书中试题中见到的树是二叉树,但并不意味着所有的树都是二叉树。)

在二叉树的概念下又衍生出满二叉树和完全二叉树的概念

满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点。也可以这样理解,除叶子结点外的所有结点均有两个子结点。节点数达到最大值,所有叶子结点必须在同一层上 完全二叉树:若设二叉树的深度为h,除第 h 层外,其它各层 (1~(h-1)层) 的结点数都达到最大个数,第h层所有的结点都连续集中在最左边,这就是完全二叉树。

二叉树在代码中的实现(TreeNode)

public class TreeNode{
    int value;
    TreeNode left;
    TreeNode right;
    public TreeNode(int value){
        this.value = value;
    }
}

1.2.2 二叉树的遍历

二叉树又引出了二叉树的遍历:前序遍历(先序遍历)、中序遍历、后序遍历、层次遍历以及深度优先搜索(Depth First Search,DFS)、广度优先搜索(Breadth First Search,BFS)。其中深度优先搜索相当于先序遍历,广度优先搜索相当于层次遍历。

  private static class TreeNode{
        int value;
        TreeNode left;
        TreeNode right;
        public TreeNode(int value){
            this.value = value;
        }
    }
​
    /* 构建用于测试的树,value都>=0 */
    static class TreeT{
        private TreeNode root;
​
        /* 根据数组创建树,value都>=0,value<0则代表null */
        public TreeT(int[] array){
            int NodeCount =0;
            if(array.length==0 || array[0]==-1) return;
            this.root = new TreeNode(array[0]);
​
            TreeNode p = root;
            for(int i=1;i<array.length;i++){
                if(array[i]>=0){
                    setNode(i,array[i]);
                }
            }
        }
        /* 测试 */
        public static void test(){
            /* 根据数组创建二叉树 */
            int[] array = {1,2,3,4,5,-1,6,-1,-1,9};
            TreeT t = new TreeT(array);
​
            System.out.print("前序遍历:");
            int[] res = t.preOrder();
            for(int i=0;i<res.length;i++){
                System.out.print(res[i]+" ");
            }
            System.out.println();
​
            System.out.print("中序遍历:");
            res = t.midOrder();
            for(int i=0;i<res.length;i++){
                System.out.print(res[i]+" ");
            }
            System.out.println();
​
            System.out.print("后序遍历:");
            res = t.backOrder();
            for(int i=0;i<res.length;i++){
                System.out.print(res[i]+" ");
            }
            System.out.println();
​
            System.out.print("层次遍历:");
            res = t.levelOrder();
            for(int i=0;i<res.length;i++){
                System.out.print(res[i]+" ");
            }
            System.out.println();
        }
        /* 更改或者创建对应的树节点 */
        public  void setNode(int index,int value){
            if(index==0){
                this.root.value = value;
            }
            int temp = index%2;
            index = (index-1)/2;
​
            TreeNode parentNode = getNode(index);
            //System.out.println(value+"---"+temp+index+parentNode.value);
if(parentNode ==null){
                throw new RuntimeException("父节点为空!"+index);
            }
            else if(temp==1){
                if(parentNode.left==null){
                    parentNode.left = new TreeNode(value);
                }else{
                    parentNode.left.value = value;
                }
            }
            else if(temp==0){
                if(parentNode.right==null){
                    parentNode.right = new TreeNode(value);
                }else{
                    parentNode.right.value = value;
                }
            }
​
        }
        /* 找到数组下标对应的树节点 */
        public TreeNode getNode(int index){
            if(index==0) return root;
            int temp = index%2;
            index = (index-1)/2;
​
            TreeNode parentNode = getNode((index-1)/2);
​
            if(parentNode==null){
                return null;
            }
            else if(temp == 1){
                return parentNode.left;
            }
            else{
                return parentNode.right;
            }
        }
​
​
        /* 先序遍历-深度优先搜索:使用递归 */
        public int[] preOrder(){
            LinkedList<Integer> list = new LinkedList<Integer>();
            preOrder_O(this.root,list);
            Integer[] integers = Arrays.copyOfRange(list.toArray(),0,list.size(),Integer[].class);
​
            return Arrays.stream(integers).mapToInt(Integer::valueOf).toArray();
        }
​
        public void preOrder_O(TreeNode root,LinkedList<Integer> list){
            if(root==null) return;
            list.add(root.value);
            //System.out.println(root.value);
            preOrder_O(root.left,list);
            preOrder_O(root.right,list);
​
        }
        /* 中序遍历 */
        public int[] midOrder(){
            LinkedList<Integer> list = new LinkedList<Integer>();
            midOrder_O(this.root,list);
            Integer[] integers = Arrays.copyOfRange(list.toArray(),0,list.size(),Integer[].class);
​
            return Arrays.stream(integers).mapToInt(Integer::valueOf).toArray();
        }
​
        public void midOrder_O(TreeNode root,LinkedList<Integer> list){
            if(root==null) return;
            midOrder_O(root.left,list);
            list.add(root.value);
            //System.out.println(root.value);
            midOrder_O(root.right,list);
​
        }
​
        /* 后序遍历 */
        public int[] backOrder(){
            LinkedList<Integer> list = new LinkedList<Integer>();
            backOrder_O(this.root,list);
            Integer[] integers = Arrays.copyOfRange(list.toArray(),0,list.size(),Integer[].class);
​
            return Arrays.stream(integers).mapToInt(Integer::valueOf).toArray();
        }
​
        public void backOrder_O(TreeNode root,LinkedList<Integer> list){
            if(root==null) return;
            backOrder_O(root.left,list);
            backOrder_O(root.right,list);
            list.add(root.value);
            //System.out.println(root.value);
​
        }
        /* 层次遍历:使用队列比较方便 */
        /* 每一层的开始以-77标识 */
        public int[] levelOrder(){
            if(this.root==null) return new int[0];
​
            LinkedList<Integer> list = new LinkedList<Integer>();
            LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
            queue.addFirst(this.root);
            while (!queue.isEmpty()){
                list.add(-77);
                int size = queue.size();
                TreeNode p;
​
                for(int i=0;i<size;i++){
​
                    p = queue.pollLast();
                    list.add(p.value);
                    if(p.left!=null){
                        queue.addFirst(p.left);
                    }
                    if(p.right!=null){
                        queue.addFirst(p.right);
                    }
                }
​
            }
​
            Integer[] integers = Arrays.copyOfRange(list.toArray(),0,list.size(),Integer[].class);
​
            return Arrays.stream(integers).mapToInt(Integer::valueOf).toArray();
        }
​
    }

1.2.3 顺序存储二叉树

除了以上定义的TreeNode所构建的树的格式,还可以将树存储在顺序表中,但是仅限于完全二叉树,或者像我在上面代码中做的那样子,以-1作为null值,但是操作起来会更加复杂。

以数组存储树,一个基本的概念是:数组下标i的树节点,它的左子节点(如果有)的下标是i*2+1,右子节点(如果有)的下标是i*2+2,它的父节点(如果有)的下标是(i-1)/2(代码中向下取整)。

使用顺序表存储的二叉树有一个经典的应用---堆排序。

堆排序的代码过程(参考 树结构_堆排序)是:

1)建立大根堆(父节点的值必定>子节点的值的完全二叉树):从叶子节点开始调整,每次操作的节点的左右子树必定已经是大根堆,只需要考虑操作节点落在哪里。

2)对调操作,脱落操作,重建操作:将大根堆的顶部对调到数组末尾并脱离树结构,将剩余节点重构大根堆,循环往复。

package cn.dataStructureAndAlgorithm.demo.sort;
 
import java.util.Arrays;
 
public class 堆排序_heapSort {
    public static void main(String[] args) {
        int data[]=new int[]{10,6,8,5,9};
        heapSort(data);
        System.out.println(Arrays.toString(data));
    }
    public static void heapSort(int data[]){
        int temp;//对调操作的中介变量
        //创建大顶堆
        //data.length/2-1可以找到最后一个父节点,不断找到父节点,并将创建大顶堆
        for (int i=data.length/2-1;i>=0;i--){
            adjustHeap(data,i,data.length);
        }
        //对调操作,脱落操作,重建操作
        for (int i=data.length-1;i>0;i--){
            //对调
            temp=data[i];
            data[i]=data[0];
            data[0]=temp;
            adjustHeap(data,0,i);//脱落,从根节点重建
        }
    }
 
    /**
     *对传入的数组,根据父节点索引,长度,进行大顶堆的创建
     * @param data 待创建大顶堆的数组
     * @param father 父节点索引
     * @param length 数组长度
     */
    public static void adjustHeap(int data[],int father,int length){
        int tempFather=data[father];//暂存父节点的值
        //对父节点其所在子树进行大顶堆的建立
        for (int i=father*2+1;i<length;i=i*2+1){//i代表左子节点,但i不能下标过界;一轮完成之后,对子节点所在的子树再重复操作
            if (i+1<length && data[i]<data[i+1]){//i+1代表右子节点,但i+1不能下标越界,但符合条件时,意味着右节点更大
                i++;//i指向右子节点
            }
            //此时i指向的都是最大子节点,接下来将最大子节点与父节点进行判断
            if (data[i]>tempFather){//当比父节点更大时
                data[father]=data[i];//最大节点放到父节点上
                father=i;//重置父节点指向为之前移动的最大节点,对子节点所在的子树再重复操作
            }else {
                break;//当没有比父节点大时,说明后续的树已经排好了,停止循环
            }
        }
        //循环完成后,father指向需要与父节点替换的最大子节点上,所以要进行替换
        data[father]=tempFather;
    }
}

1.3 二叉查找树

二叉查找树是二叉树的衍生概念:

二叉查找树(英语:Binary Search Tree),也称为二叉搜索树、有序二叉树(ordered binary tree)或排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:

  1. 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;

  2. 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;

  3. 任意节点的左、右子树也分别为二叉查找树;

  4. 没有键值相等的节点。

    二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低为 O ( log ⁡ n ) 。二叉查找树是基础性数据结构,用于构建更为抽象的数据结构,如集合、多重集、关联数组等。

     

    二叉查找树总的来说就是其左节点的值<父节点的值<右节点的结构,最简单的构建就是在添加时判断大小,顺着左右节点往下添加。但是这样子就很容易陷入一些极端情况导致树的结构极端不平衡,其查找效率也达不到二分查找的效果(比如说,一棵只有左节点的二叉查找树),由此引出平衡二叉树的概念。

1.4 平衡二叉树(AVL树)

平衡二叉树:当且仅当任何节点的两棵子树的高度差不大于1的二叉树

其中AVL树是最先发明的自平衡二叉查找树,是最原始典型的平衡二叉树。

平衡二叉树是基于二叉查找树的改进。由于在某些极端的情况下(如在插入的序列是有序的时),二叉查找树将退化成近似链或链,此时,其操作的时间复杂度将退化成线性的,即O(n)。所以我们通过自平衡操作(即旋转)构建两个子树高度差不超过1的平衡二叉树。

AVL树是在查找二叉树的基础上增加了判断左右子树高度差,以及加入左旋、右旋、自旋的操作。具体过程参考:树结构_AVL树(平衡二叉树),红黑树与B系列树简介

1.5 红黑树

1.5.1 红黑树的定义

红黑树也是一种自平衡的二叉查找树。

  1. 每个结点要么是红的要么是黑的。(红或黑)

  2. 根结点是黑的。 (根黑)

  3. 每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。 (叶黑)

  4. 如果一个结点是红的,那么它的两个儿子都是黑的。 (红子黑)

  5. 对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。(路径下黑相同)

     

 

如图就是一棵典型的红黑树。保证红黑树满足它的基本性质,就是在调整数据结构自平衡。

而红黑树自平衡的调整操作方式就有旋转和变色两种。

红黑树是一种应用很广的数据结构,如在Java集合类中TreeSet和TreeMap的底层,C++STL中set与map,以及linux中虚拟内存的管理。

1.5.2 红黑树生成过程

  1、当插入的节点的父节点为null,则将该节点颜色置为black。

  2、当插入节点的父节点颜色为black,不需要调整。

  3、当插入节点的父节点为red,其叔父节点亦为红色,则将其父亲节点和叔父节点置为black,同时将祖父节点置为red,将祖父节点设置为当前新增节点,重新按照从规则1开始判断。

  4、但插入节点的父亲节点为red,其叔父节点为black或null,则需要分多钟情况考虑。

    a)新增节点为父亲节点右孩子同时父亲节点是祖父节点的左孩子,则进行左旋,将父节点置为新节点,重新按照规则1进行判断;

    b)新增节点为父亲节点左孩子,同时父亲节点是租户节点的右孩子,则进行右旋,将父节点置为新节点,重新按照规则1进行判断;

  5、不满足上述所有条件,将父节点置为black,同时,将祖父节点置为red,进行以下两种情况判断。

    a)如果新增节点是父亲节点的左孩子,同时,父亲节点是祖父孩子的左孩子,则对祖父节点进行右旋

    b)其他情况,对祖父节点左旋。

这里有一个演示红黑树生成过程的网址:https://www.cs.usfca.edu/~galles/visualization/RedBlack.html

以65 77 79 1 2 3 4 5 为例

  • 插入65,触发了条件1

     

  • 插入77,触发了条件2

     

  • 插入79,触发条件5b)

 

  • 插入1,触发条件3,然后将祖父节点77作为新增节点判断触发条件1

 

  • 插入2,触发条件4,然后以父节点1作为新增节点判断触发5a),但是下面第三个图应该是错了,应该是先换色再右旋得到图四。

     

  • 插入3,触发条件3

 

  • 插入4,触发条件4,然后以父节点3作为新增节点判断,触发条件5

 

  • 插入5,触发条件3,并将祖父节点4看作新增节点

     

     

    换成将组父节点4视为新增节点进行判断,触发4a),左旋,然后将4的父节点2看作新增节点

     

     

     

    换成将父节点2视为新增节点进行判断,触发5a),先更改颜色,再整体右旋

     

     

 

1.5.3 红黑树和平衡二叉树的区别

红黑树是一种自平衡二叉查找树,红黑树和AVL都是BST(二叉排序树)的平衡版本,相比于AVL的完全平衡,红黑树只要求局部平衡,因此当向红黑树插入和删除结点时,需要调整的比AVL要少,统计性能要高于AVL树,C++ STL中的map、set、multimap和multiset都应用了红黑树的变体。

下面是平衡二叉树的生成过程。65 77 79 1 2 3 4 5

 

1.6 哈夫曼树(Huffman Tree)

哈夫曼树是一种带权路径长度最短的二叉树,也称为最优二叉树。

一般可以按下面步骤构建:

1,将所有左,右子树都为空的作为根节点。 2,在森林中选出两棵根节点的权值最小的树作为一棵新树的左,右子树,且置新树的附加根节点的权值为其左,右子树上根节点的权值之和。注意,左子树的权值应小于右子树的权值。 3,从森林中删除这两棵树,同时把新树加入到森林中。 4,重复2,3步骤,直到森林中只有一棵树为止,此树便是哈夫曼树。

大家可能更多听说的是哈夫曼编码,其实就是哈夫曼树的应用。即如何让电文中出现较多的字符采用尽可能短的编码且保证在译码时不出现歧义。

 

1.7 多路查找树

大规模数据存储中,实现索引查询这样一个实际背景下,树节点存储的元素数量是有限的(如果元素数量非常多的话,查找就退化成节点内部的线性查找了),这样导致二叉查找树结构由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下。

多路查找树常用的变体包含:B树(B-树)、B+树、B*树、R树

1.8 B树

B树(英语:B-tree)是一种自平衡的树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。B树,概括来说是一个一般化的二叉查找树(binary search tree),可以拥有最多2个子节点。与自平衡二叉查找树不同,B树适用于读写相对大的数据块的存储系统,例如磁盘。

1.根结点至少有两个子女。

2.每个中间节点都包含k-1个元素和k个孩子,其中 m/2 <= k <= m

3.每一个叶子节点都包含k-1个元素,其中 m/2 <= k <= m

4.所有的叶子结点都位于同一层。

5.每个节点中的元素从小到大排列,节点当中k-1个元素正好是k个孩子包含的元素的值域分划。

 

如图所示就是一颗符合规范的B树,由于相比于磁盘IO的速度,内存中的耗时几乎可以省略,所以只要树的高度足够低,IO次数足够小,就可以提升查询性能。

B树的增加删除同样遵循自平衡的性质,有旋转和换位。

B树的应用是文件系统及部分非关系型数据库索引

1.9 B+树

B+ 树是一种树数据结构,通常用于关系型数据库(如Mysql)和操作系统的文件系统中。B+ 树的特点是能够保持数据稳定有序,其插入与修改拥有较稳定的对数时间复杂度。B+ 树元素自底向上插入,这与二叉树恰好相反。

在B树基础上,为叶子结点增加链表指针(B树+叶子有序链表),所有关键字都在叶子结点 中出现,非叶子结点作为叶子结点的索引;B+树总是到叶子结点才命中。

b+树的非叶子节点不保存数据,只保存子树的临界值(最大或者最小),所以同样大小的节点,b+树相对于b树能够有更多的分支,使得这棵树更加矮胖,查询时做的IO操作次数也更少。

 

 

这通常在多数节点在次级存储比如硬盘中的时候出现。通过最大化在每个内部节点内的子节点的数目减少树的高度,平衡操作不经常发生,而且效率增加了。

1.10 B*树

B*树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针

在B+树基础上,为非叶子结点也增加链表指针,将结点的最低利用率从1/2提高到2/3。

1.11 R树

 

R树是用来做空间数据存储的树状数据结构。例如给地理位置,矩形和多边形这类多维数据建立索引。

R树的核心思想是聚合距离相近的节点并在树结构的上一层将其表示为这些节点的最小外接矩形(MBR),这个最小外接矩形就成为上一层的一个节点。因为所有节点都在它们的最小外接矩形中,所以跟某个矩形不相交的查询就一定跟这个矩形中的所有节点都不相交。叶子节点上的每个矩形都代表一个对象,节点都是对象的聚合,并且越往上层聚合的对象就越多。也可以把每一层看做是对数据集的近似,叶子节点层是最细粒度的近似,与数据集相似度100%,越往上层越粗糙。 

 

1.X 参考

数据结构之树

二叉树的深度优先遍历(先序遍历)和广度优先遍历(层次遍历)

数据结构与算法整理总结目录 :>

树结构_二叉树基础,顺序存储二叉树,线索化二叉树

树结构_堆排序

红黑树与AVL(平衡二叉树)的区别

红黑树基本特点,及其建立——转

Data Structure Visualizations

 

 

2、Map

这里是之前整理集合的时候的一些记录,只是通常不重点关注某个部分都是草草掠过,然后现在再看一下Map。

  1. JAVA篇:Java集合框架

  2. JAVA篇:Java 多线程 (三) 多线程与集合

 

Map接口的实现类中包含了几个比较重要的:

  • TreeMap: 一个有序的key-value集合,非同步,基于红黑树(Red-Black tree)实现。

  • HashMap:基于哈希表的Map接口的实现。此实现提供所有可选的映射操作,并允许使用null值和null键。除了非同步和允许使用null之外,HashMap类与Hashtable大致相同,此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

  • LinkedHashMap:是HashMap的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap。

    LinkedHashMap维护着一个运行于所有条目的双重链接列表,此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。

  • Hashtable:该类实现了一个哈希表,它将键映射到值。 任何非null对象都可以用作键值或值。 这个类与HashMap相似,只是内嵌了synchronized,是线程安全的类。

  • ConcurrentHashMap:支持检索的完全并发性和更新的高预期并发性的哈希表。 这个类服从相同功能规范如Hashtable ,并且包括对应于每个方法的方法版本Hashtable 。 不过,尽管所有操作都是线程安全的,检索操作并不意味着锁定,并没有为防止所有访问的方式锁定整个表的任何支持。 这个类可以在依赖于线程安全性的程序中与Hashtable完全互Hashtable ,但不依赖于其同步细节。

     

3、HashMap和ConcurrentHashMap

太棒了!HashMap和 ConcurrentHashMap的问题终于总结清楚了

HashMap?ConcurrentHashMap?相信看完这篇没人能难住你!

3.1 哈希表

哈希表的主体是一个数组,根据计算得到的哈希值将数据放入指定位置,在没有发生冲突的情况下,其新增及查找的时间复杂度都是O(1)。

但是数据在插入的时候可能会出现哈希冲突,即多个数据的哈希值定位到数组的同一个位置。哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法.

  • JDK7 使用了数组+链表的方式

  • JDK8 使用了数组+链表+红黑树的方式

 

 

 

 

 

3.2 HashMap

HashMap 底层是基于 数组 + 链表 组成的,不过在 jdk1.7 和 1.8 中具体实现稍有不同。

在1.7中,HashMap底层结构如下所示,所以当冲突数变多之后,链表部分就会越来越长,查询效率过低。

 

在1.8中增加了红黑树的实现,通过设置TREEIFY_THRESHOLD阈值判断,当链表长度超出阈值则进行红黑树的实现。

 

 

 

  • 发生冲突关于entry节点插入链表还是链头呢? JDK7:插入链表的头部,头插法 JDK8:插入链表的尾部,尾插法

     

4、ConcurrentHashMap

4.1 ConcurrentHashMap

ConcurrentHashMap<K,V>定义在java.util.concurrent下,实现了子接口ConcurrentMap<K,V>,提供线程安全和原子性保证。

JDK1.7的结构图如下所示,,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。

 

原理上来说:ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。

JDK1.8 抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。JDK1.8 也抛弃了HashEntry 结构,采用了Node结构(及支撑红黑树的TreeNode结构),与HashEntry 相似,只能进行查找,不能进行修改。

 

 

1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。

4.2 Synchronized容器和Concurrent容器

HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。

HashMap并非是线程安全的结构,由此引出了HashTable和ConcurrentHashMap。

HashTable和ConcurrentHashMap都是线程安全的,但是HashTable线程操作时是对整个数组加锁,属于Synchronized容器,而ConcurrentHashMap属于Concurrent容器,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry

在Java语言中,多线程安全的容器主要分为两种:Synchronized和Concurrent,虽然它们都是线程安全的,但是它们在性能方面差距比较大。

Synchronized容器(同步容器)主要通过synchronized关键字来实现线程安全,在使用的时候会对所有的数据加锁。需要注意的是,由于同步容器将所有对容器状态的访问都串行化了,这样虽然保证了线程的安全性,但是这种方法的代价就是严重降低了并发性,当多个线程竞争容器时,吞吐量会严重降低。于是引入了Concurrent容器(并发容器),Concurrent容器采用了更加智能的方案,该方案不是对整个数据加锁,而是采取了更加细粒度的锁机制,因此,在大并发量的情况下,拥有更高的效率。

 

X 参考

map详解

JDK7 中hashMap链表成环的分析过程与JDK8的解决方案

太棒了!HashMap和 ConcurrentHashMap的问题终于总结清楚了

Hashtable和ConcurrentHashMap的区别

当你深入了解,你就会发现世界如此广袤,而你对世界的了解则是如此浅薄,请永远保持谦卑的态度。
原文地址:https://www.cnblogs.com/liwxmyself/p/15552448.html