面霸篇,从面试角度作为切入点晋升大家的 Java 内功,所谓根基不牢,地动山摇

码哥在 《Redis 系列》的开篇 Redis 为什么这么快中说过:学习一个技术,通常只接触了零散的技术点,没有在脑海里建设一个残缺的常识框架和架构体系,没有零碎观。这样会很吃力,而且会呈现一看如同本人会,过后就遗记,一脸懵逼。

咱们须要一个零碎观,清晰残缺的去学习技术,在「面霸篇:Java 外围根底大满贯(卷一)」中,码哥梳理了 Java 高频外围知识点。

本篇将一举攻破 Java 汇合容器知识点,跟着「码哥」一起来提纲挈领,梳理一个残缺的 Java 容器开发技术能力图谱,将根底夯实。

[toc]

点击下方卡片,关注「码哥字节」

汇合容器概述

什么是汇合?

顾名思义,汇合就是用于存储数据的容器

汇合框架是为示意和操作汇合而规定的一种对立的规范的体系结构。 任何汇合框架都蕴含三大块内容:对外的接口、接口的实现和对汇合运算的算法

码老湿,能够说下汇合框架的三大块内容具体指的是什么吗?

接口

面向接口编程,形象出汇合类型,使得咱们能够在操作汇合的时候不用关怀具体实现,达到「多态」。

就好比密码箱,咱们只关怀能关上箱子,寄存货色,并且敞开箱子,至于怎么加密咱们不关怀。

接口实现

每种汇合的具体实现,是重用性很高的数据结构。

算法

汇合提供了数据寄存以及查找、排序等性能,汇合有很多种,也就是算法通常也是多态的,因为雷同的办法能够在同一个接口被多个类实现时有不同的体现

事实上,算法是可复用的函数。 它缩小了程序设计的辛苦。

汇合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要局部上,而不是为了让程序能失常运行而将注意力于低层设计上。

汇合的特点

  • 对象封装数据,多个对象须要用汇合存储;
  • 对象的个数能够确定应用数组更高效,不确定个数的状况下能够应用汇合,因为汇合是可变长度。

汇合与数组的区别

  • 数组是固定长度的;汇合可变长度的。
  • 数组能够存储根本数据类型,也能够存储援用数据类型;汇合只能存储援用数据类型。
  • 数组存储的元素必须是同一个数据类型;汇合存储的对象能够是不同数据类型。

因为有多种汇合容器,因为每一个容器的本身特点不同,其实原理在于每个容器的外部数据结构不同。

汇合容器在一直向上抽取过程中,呈现了汇合体系。在应用一个体系的准则:参阅顶层内容。建设底层对象。

汇合框架有哪些劣势

  • 容量主动增长扩容;
  • 提供高性能的数据结构和算法;
  • 能够不便地扩大或改写汇合,进步代码复用性和可操作性。
  • 通过应用JDK自带的汇合类,能够升高代码保护和学习新API老本。

有哪些罕用的汇合类

Java 容器分为 Collection 和 Map 两大类,Collection汇合的子接口有Set、List、Queue三种子接口。

咱们比拟罕用的是Set、List,Map接口不是 collection的子接口。

Collection汇合次要有List和Set两大接口

  • List:一个有序(元素存入汇合的程序和取出的程序统一)容器,元素能够反复,能够插入多个null元素,元素都有索引。罕用的实现类有 ArrayList、LinkedList 和 Vector。
  • Set:一个无序(存入和取出程序有可能不统一)容器,不能够存储反复元素,只容许存入一个null元素,必须保障元素唯一性。
  • Set 接口罕用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

Map是一个键值对汇合,存储键、值和之间的映射。 Key无序,惟一;value 不要求有序,容许反复。

Map没有继承于 Collection 接口,从 Map 汇合中检索元素时,只有给出键对象,就会返回对应的值对象。

Map 的罕用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap

汇合的底层数据结构

Collection

  1. List

    • ArrayList:Object 数组;
    • Vector:Object 数组;
    • LinkedList:双向循环链表;
  2. Set

    • HashSet:惟一,无序。基于 HashMap 实现,底层采纳 HashMap 保留数据。

      它不容许汇合中有反复的值,当咱们提到HashSet时,第一件事件就是在将对象存储在HashSet之前,要先确保对象重写equals()和hashCode()办法,这样能力比拟对象的值是否相等,以确保set中没有贮存相等的对象。

      如果咱们没有重写这两个办法,将会应用这个办法的默认实现。

    • LinkedHashSet: LinkedHashSet 继承与 HashSet,底层应用 LinkedHashMap 来保留所有元素。
    • TreeSet(有序,惟一): 红黑树(自均衡的排序二叉树。)

Map

  • HashMap:JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是次要为了解决哈希抵触而存在的(“拉链法”解决抵触)。

    JDK1.8当前在解决哈希抵触时有了较大的变动,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以缩小搜寻工夫。

  • LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层依然是基于拉链式散列构造即由数组和链表或红黑树组成

    外部还有一个双向链表保护键值对的程序,每个键值对既位于哈希表中,也位于双向链表中

    LinkedHashMap反对两种程序插入程序 、 拜访程序。

    • 插入程序:先增加的在后面,后增加的在前面。批改操作不影响程序
    • 拜访程序:所谓拜访指的是get/put操作,对一个键执行get/put操作后,其对应的键值对会挪动到链表开端,所以最开端的是最近拜访的,最开始的是最久没有被拜访的,这就是拜访程序。
  • HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是次要为了解决哈希抵触而存在的
  • TreeMap: 红黑树(自均衡的排序二叉树)

汇合的 fail-fast 疾速失败机制

Java 汇合的一种谬误检测机制,当多个线程对汇合进行构造上的扭转的操作时,有可能会产生 fail-fast 机制。

起因:迭代器在遍历时间接拜访汇合中的内容,并且在遍历过程中应用一个 modCount 变量。

汇合在被遍历期间如果内容发生变化,就会扭转modCount的值。

每当迭代器应用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异样,终止遍历。

解决办法:

  1. 在遍历过程中,所有波及到扭转modCount值得中央全副加上synchronized。
  2. 应用CopyOnWriteArrayList来替换ArrayList

Collection 接口

List 接口

Itertator 是什么

Iterator 接口提供遍历任何 Collection 的接口。咱们能够从一个 Collection 中应用迭代器办法来获取迭代器实例。

迭代器取代了 Java 汇合框架中的 Enumeration,迭代器容许调用者在迭代过程中移除元素。

List<String> list = new ArrayList<>();Iterator<String> it = list. iterator();while(it. hasNext()){  String obj = it. next();  System. out. println(obj);}

如何边遍历边移除 Collection 中的元素?

Iterator<Integer> it = list.iterator();while(it.hasNext()){   *// do something*   it.remove();}

一种最常见的谬误代码如下:

for(Integer i : list){   list.remove(i)}

运行以上错误代码会报 ConcurrentModificationException 异样

如何实现数组和 List 之间的转换?

  • 数组转 List:应用 Arrays. asList(array) 进行转换。
  • List 转数组:应用 List 自带的 toArray() 办法。

ArrayList 和 LinkedList 的区别是什么?

  • 数据结构实现:ArrayList 是动静数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
  • 随机拜访效率:ArrayList 比 LinkedList 在随机拜访的时候效率要高,因为 LinkedList 是线性的数据存储形式,所以须要挪动指针从前往后顺次查找。
  • 减少和删除效率:在非首尾的减少和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其余数据的下标。
  • 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个援用,一个指向前一个元素,一个指向后一个元素。
  • 线程平安:ArrayList 和 LinkedList 都是不同步的,也就是不保障线程平安;

综合来说,在须要频繁读取汇合中的元素时,更举荐应用 ArrayList,而在插入和删除操作较多时,更举荐应用 LinkedList。

为什么 ArrayList 的 elementData 加上 transient 润饰?

ArrayList 中的数组定义如下:

private transient Object[] elementData;

ArrayList 的定义:

public class ArrayList<E> extends AbstractList<E>     implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList 实现了 Serializable 接口,这意味着 ArrayList 反对序列化。

transient 的作用是说不心愿 elementData 数组被序列化。

每次序列化时,先调用 defaultWriteObject() 办法序列化 ArrayList 中的非 transient 元素,而后遍历 elementData,只序列化已存入的元素,这样既放慢了序列化的速度,又减小了序列化之后的文件大小。

介绍下CopyOnWriteArrayList?

CopyOnWriteArrayList是ArrayList的线程平安版本,也是赫赫有名的copy-on-write(COW,写时复制)的一种实现。

在读操作时不加锁,跟ArrayList相似;在写操作时,复制出一个新的数组,在新数组上进行操作,操作完了,将底层数组指针指向新数组。

适宜应用在读多写少的场景。例如add(Ee)办法的操作流程如下:应用ReentrantLock加锁,拿到原数组的length,应用Arrays.copyOf办法从原数组复制一个新的数组(length+1),将要增加的元素放到新数组的下标length地位,最初将底层数组指针指向新数组。

List、Set、Map三者的区别?

  • List(凑合程序的好帮手):存储的对象是可反复的、有序的。
  • Set(重视举世无双的性质):存储的对象是不可反复的、无序的。
  • Map(用Key来搜寻的专业户):存储键值对(key-value),不能蕴含反复的键(key),每个键只能映射到一个值。

Set 接口

说一下 HashSet 的实现原理?

  • HashSet底层原理齐全就是包装了一下HashMap
  • HashSet的唯一性保障是依赖与hashCode()equals()两个办法,所以存入对象的时候肯定要本人重写这两个办法来设置去重的规定。
  • HashSet 中的元素都寄存在 HashMap key 下面,而value 中的值都是对立的一个 private static final Object PRESENT = new Object();

hashCode()与equals()的相干规定

  1. 如果两个对象相等,则 hashcode 肯定也是雷同的
  2. 两个对象相等,对两个 equals 办法返回 true
  3. 两个对象有雷同的 hashcode 值,它们也不肯定是相等的
  4. 综上,equals办法被笼罩过,则hashCode办法也必须被笼罩
  5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即便这两个对象指向雷同的数据)。

==与equals的区别

  1. == 是判断两个变量或实例是不是指向同一个内存空间 equals 是判断两个变量或实例所指向的内存空间的值是不是雷同
  2. == 是指对内存地址进行比拟 equals() 是对字符串的内容进行比拟
  3. ==指援用是否雷同, equals() 指的是值是否雷同。

Queue

BlockingQueue是什么?

Java.util.concurrent.BlockingQueue 是一个队列,在进行检索或移除一个元素的时候,线程会期待队列变为非空;

当在增加一个元素时,线程会期待队列中的可用空间。

BlockingQueue接口是Java汇合框架的一部分,次要用于实现生产者-消费者模式。

Java提供了几种 BlockingQueue 的实现,比方ArrayBlockingQueueLinkedBlockingQueuePriorityBlockingQueueSynchronousQueue等。

在 Queue 中 poll()和 remove()有什么区别?

  • 相同点:都是返回第一个元素,并在队列中删除返回的对象。
  • 不同点:如果没有元素 poll()会返回 null,而 remove()会间接抛出 NoSuchElementException 异样。

Map 接口

Map 整体构造如下所示:

Hashtable 比拟特地,作为相似 Vector、Stack 的晚期汇合相干类型,它是扩大了 Dictionary 类的,类构造上与 HashMap 之类显著不同。

HashMap 等其余 Map 实现则是都扩大了 AbstractMap,外面蕴含了通用办法形象。

不同 Map 的用处,从类图构造就能体现进去,设计目标曾经体现在不同接口上。

HashMap 的实现原理?

在 JDK 1.7 中 HashMap 是以数组加链表的模式组成的,JDK 1.8 之后新增了红黑树的组成构造,当链表大于 8 并且容量大于 64 时,链表构造会转换成红黑树结构。

HashMap 基于 Hash 算法实现的:

  1. 当咱们往Hashmap 中 put 元素时,利用 key 的 hashCode 从新 hash 计算出以后对象的元素在数组中的下标。
  2. 存储时,如果呈现 hash 值雷同的 key,此时有两种状况。

    • 如果 key 雷同,则笼罩原始值;
    • 如果 key 不同(呈现抵触),则将以后的 key-value 放入链表中
  3. 获取时,间接找到 hash 值对应的下标,在进一步判断 key 是否雷同,从而找到对应值。
  4. 了解了以上过程就不难明确 HashMap 是如何解决 hash 抵触的问题,外围就是应用了数组的存储形式,而后将抵触的key的对象放入链表中,一旦发现抵触就在链表中做进一步的比照。

JDK1.7 VS JDK1.8 比拟

JDK1.8次要解决或优化了一下问题:

  1. resize 扩容优化
  2. 引入了红黑树,目标是防止单条链表过长而影响查问效率,红黑树算法请参考
  3. 解决了多线程死循环问题,但仍是非线程平安的,多线程时可能会造成数据失落问题。
不同JDK 1.7JDK 1.8
存储构造数组 + 链表数组 + 链表 + 红黑树
初始化形式独自函数:inflateTable()间接集成到了扩容函数resize()
hash值计算形式扰动解决 = 9次扰动 = 4次位运算 + 5次异或运算扰动解决 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规定无抵触时,寄存数组;抵触时,寄存链表无抵触时,寄存数组;抵触 & 链表长度 < 8:寄存单链表;抵触 & 链表长度 > 8:树化并寄存红黑树
插入数据形式头插法(先讲原地位的数据移到后1位,再插入数据到该地位)尾插法(直接插入到链表尾部/红黑树)
扩容后存储地位的计算形式全副依照原来办法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1))依照扩容后的法则计算(即扩容后的地位=原地位 or 原地位 + 旧容量)

如何无效防止哈希碰撞

次要是因为如果应用hashCode取余,那么相当于参加运算的只有hashCode的低位,高位是没有起到任何作用的。

所以咱们的思路就是让 hashCode 取值出的高位也参加运算,进一步升高hash碰撞的概率,使得数据分布更均匀,咱们把这样的操作称为扰动,在JDK 1.8中的hash()函数如下:

static final int hash(Object key) {    int h;    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);// 与本人右移16位进行异或运算(高下位异或)}

HashMap的put办法的具体流程?

当咱们put的时候,首先计算 keyhash值,这里调用了 hash办法,hash办法理论是让key.hashCode()key.hashCode()>>>16进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大略的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目标是缩小碰撞

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.依据键值key计算hash值得到插入的数组索引i,如果table[i]==null,间接新建节点增加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果雷同间接笼罩value,否则转向④,这里的雷同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则间接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key曾经存在间接笼罩value即可;

⑥.插入胜利后,判断理论存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

HashMap的扩容操作是怎么实现的?

①.在jdk1.8中,resize办法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize办法进行扩容;

②.每次扩大的时候,都是扩大2倍;

③.扩大后Node对象的地位要么在原地位,要么挪动到原偏移量两倍的地位。

在1.7中,扩容之后须要从新去计算其Hash值,依据Hash值对其进行散发.

但在1.8版本中,则是依据在同一个桶的地位中进行判断(e.hash & oldCap)是否为0,0 -示意还在原来地位,否则就挪动到原数组地位 + oldCap。

从新进行 hash 调配后,该元素的地位要么停留在原始地位,要么挪动到原始地位+减少的数组大小这个地位上。

任何类都能够作为 Key 么?

能够应用任何类作为 Map 的 key,然而在应用之前,须要思考以下几点:

  • 如果类重写了 equals() 办法,也应该重写 hashCode() 办法。
  • 类的所有实例须要遵循与 equals() 和 hashCode() 相干的规定。
  • 如果一个类没有应用 equals(),不应该在 hashCode() 中应用它。
  • 用户自定义 Key 类最佳实际是使之为不可变的,这样 hashCode() 值能够被缓存起来,领有更好的性能。

    不可变的类也能够确保 hashCode() 和 equals() 在将来不会扭转,这样就会解决与可变相干的问题了。

为什么HashMap中String、Integer这样的包装类适宜作为K?

String、Integer等包装类的个性可能保障Hash值的不可更改性和计算准确性,可能无效的缩小Hash碰撞的几率。

  1. 都是final类型,即不可变性,保障key的不可更改性,不会存在获取hash值不同的状况
  2. 外部已重写了equals()hashCode()等办法,恪守了HashMap外部的标准(不分明能够去下面看看putValue的过程),不容易呈现Hash值计算错误的状况;

HashMap为什么不间接应用hashCode()解决后的哈希值间接作为table的下标?

hashCode()办法返回的是int整数类型,其范畴为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空间,而HashMap的容量范畴是在16(初始化默认值)~2 ^ 30,HashMap通常状况下是取不到最大值的,并且设施上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范畴内,进而无奈匹配存储地位;

HashMap 的长度为什么是2的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据调配平均,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。

这个算法应该如何设计呢?

咱们首先可能会想到采纳 % 取余的操作来实现。

然而,重点来了:取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。

并且采纳二进制位操作 &,绝对于 % 可能进步运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

那为什么是两次扰动呢?

答:这样就是加大哈希值低位的随机性,使得散布更平均,从而进步对应数组存储下标地位的随机性&平均性,最终缩小Hash抵触,两次就够了,曾经达到了高位低位同时参加运算的目标;

HashMap 和 ConcurrentHashMap 的区别

  1. ConcurrentHashMap对整个桶数组进行了宰割分段(Segment),每一个分段上都用lock锁进行爱护,绝对于HashTable的synchronized锁的粒度更精密了一些,并发性能更好,而HashMap没有锁机制,不是线程平安的。(JDK1.8之后ConcurrentHashMap启用了一种全新的形式实现,利用 synchronized + CAS算法。)
  2. HashMap的键值对容许有null,然而ConCurrentHashMap都不容许。

ConcurrentHashMap 实现原理

JDK1.7

首先将数据分为一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁拜访其中一个段数据时,其余段的数据也能被其余线程拜访。

在JDK1.7中,ConcurrentHashMap采纳Segment + HashEntry的形式进行实现,构造如下:

一个 ConcurrentHashMap 里蕴含一个 Segment 数组。

Segment 的构造和HashMap相似,是一种数组和链表构造,一个 Segment 蕴含一个 HashEntry 数组,每个 HashEntry 是一个链表构造的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行批改时,必须首先取得对应的 Segment的锁。

  1. 该类蕴含两个动态外部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;
  2. HashEntry 外部应用 volatile 的 value 字段来保障可见性,get 操作须要保障的是可见性,所以并没有什么同步逻辑。
  3. Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行批改时,必须首先取得对应的 Segment 锁。

get 操作须要保障的是可见性,所以并没有什么同步逻辑

public V get(Object key) {        Segment<K,V> s; // manually integrate access methods to reduce overhead        HashEntry<K,V>[] tab;        int h = hash(key.hashCode());       //利用位操作替换一般数学运算       long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;        // 以Segment为单位,进行定位        // 利用Unsafe间接进行volatile access        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&            (tab = s.table) != null) {           //省略          }        return null;    }

而对于 put 操作,首先是通过二次哈希防止哈希抵触,而后以 Unsafe 调用形式,间接获取相应的 Segment,而后进行线程平安的 put 操作:

 public V put(K key, V value) {        Segment<K,V> s;        if (value == null)            throw new NullPointerException();        // 二次哈希,以保证数据的分散性,防止哈希抵触        int hash = hash(key.hashCode());        int j = (hash >>> segmentShift) & segmentMask;        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment            s = ensureSegment(j);        return s.put(key, hash, value, false);    }

其外围逻辑实现在上面的外部办法中:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {            // scanAndLockForPut会去查找是否有key雷同Node            // 无论如何,确保获取锁            HashEntry<K,V> node = tryLock() ? null :                scanAndLockForPut(key, hash, value);            V oldValue;            try {                HashEntry<K,V>[] tab = table;                int index = (tab.length - 1) & hash;                HashEntry<K,V> first = entryAt(tab, index);                for (HashEntry<K,V> e = first;;) {                    if (e != null) {                        K k;                        // 更新已有value...                    }                    else {                        // 搁置HashEntry到特定地位,如果超过阈值,进行rehash                        // ...                    }                }            } finally {                unlock();            }            return oldValue;        }

JDK1.8

JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采纳Node + CAS + Synchronized来保障并发平安进行实现

synchronized只锁定以后链表或红黑二叉树的首节点,这样只有hash不抵触,就不会产生并发,效率又晋升N倍。

  • 总体构造上,它的外部存储和 HashMap 构造十分类似,同样是大的桶(bucket)数组,而后外部也是一个个所谓的链表构造(bin),同步的粒度要更粗疏一些。
  • 其外部依然有 Segment 定义,但仅仅是为了保障序列化时的兼容性而已,不再有任何构造上的用途。
  • 因为不再应用 Segment,初始化操作大大简化,批改为 lazy-load 模式,这样能够无效防止初始开销,解决了老版本很多人埋怨的这一点。
  • 数据存储利用 volatile 来保障可见性。
  • 应用 CAS 等操作,在特定场景进行无锁并发操作。
  • 应用 Unsafe、LongAdder 之类底层伎俩,进行极其状况的优化。

另外,须要留神的是,“线程平安”这四个字特地容易让人误会,因为ConcurrentHashMap 只能保障提供的原子性读写操作是线程平安的。

误区

咱们来看一个应用 Map 来统计 Key 呈现次数的场景吧,这个逻辑在业务代码中十分常见。

开发人员误以为应用了 ConcurrentHashMap 就不会有线程平安问题,于是不加思索地写出了上面的代码:

  • 在每一个线程的代码逻辑中先通过 containsKey办法判断能够 是否存在。
  • key 存在则 + 1,否则初始化 1.
// 共享数据ConcurrentHashMap<String, Long> freqs = new ConcurrentHashMap<>(ITEM_COUNT);public void normaluse(String key) throws InterruptedException {          if (freqs.containsKey(key)) {        //Key存在则+1        freqs.put(key, freqs.get(key) + 1);      } else {        //Key不存在则初始化为1        freqs.put(key, 1L);      }}

大错特错啊敌人们,须要留神 ConcurrentHashMap 对外提供的办法或能力的限度

  • 应用了 ConcurrentHashMap,不代表对它的多个操作之间的状态是统一的,是没有其余线程在操作它的,如果须要确保须要手动加锁。
  • 诸如 size、isEmpty 和 containsValue 等聚合办法,在并发状况下可能会反映 ConcurrentHashMap 的中间状态。

    因而在并发状况下,这些办法的返回值只能用作参考,而不能用于流程管制。

    显然,利用 size 办法计算差别值,是一个流程管制。

  • 诸如 putAll 这样的聚合办法也不能确保原子性,在 putAll 的过程中去获取数据可能会获取到局部数据。

正确写法:

//利用computeIfAbsent()办法来实例化LongAdder,而后利用LongAdder来进行线程平安计数 freqs.computeIfAbsent(key, k -> new LongAdder()).increment();
  • 应用 ConcurrentHashMap 的原子性办法 computeIfAbsent 来做复合逻辑操作,判断 Key 是否存在 Value,如果不存在则把 Lambda 表达式运行后的后果放入 Map 作为 Value,也就是新创建一个 LongAdder 对象,最初返回 Value。
  • 因为 computeIfAbsent 办法返回的 Value 是 LongAdder,是一个线程平安的累加器,因而能够间接调用其 increment 办法进行累加。