Java集合容器总结

Java集合容器总结

集合框架

常见的集合有哪些?

Java集合类主要由两个根接口CollectionMap派生出来的,Collection派生出了三个子接口:ListSetQueue(Java5新增的队列),因此Java集合大致也可分成ListSetQueueMap四种接口体系。
在这里插入图片描述
在这里插入图片描述

  • List代表了有序可重复集合,可直接根据元素的索引来访问;
  • Set代表无序不可重复集合,只能根据元素本身来访问;
  • Queue是队列集合。
  • 淡绿色背景覆盖的是集合体系中常用的实现类

线程安全的集合有哪些?线程不安全的呢?

线程安全的:

  • Hashtable:比HashMap多了个线程安全。
  • ConcurrentHashMap:是一种高效但是线程安全的集合。
  • Vector:比Arraylist多了个同步化机制。
  • Stack:栈,也是线程安全的,继承于Vector。

线程不安全的:

  • HashMap
  • Arraylist
  • LinkedList
  • HashSet
  • TreeSet
  • TreeMap

Collection框架中实现比较要怎么做?

  • 实体类实现Comparable接口,并实现 compareTo(T t) 方法,称为内部比较器。
  • 创建一个外部比较器,这个外部比较器要实现Comparator接口的 compare(T t1, T t2)方法。

什么是Iterator?

Iterator是一个接口,可以使用集合的iterator()方法得到迭代器实例,使用hasNext()next()可以遍历所有集合

  • 只能从头到尾遍历集合
  • 迭代器自身没有向集合中添加、修改元素的方法,只有remove()方法可以移除元素,移除的是当前next()指向的对象
  • 使用迭代器遍历的过程中,集合自身对其内容进行修改,那么遵循快速失败和安全失败准则:
    • util包下的集合修改会报异常
    • juc包下的集合修改不会报异常,但在next()指针之前修改的元素不会遍历到,如1234,第一个指针指向1,此时删除1,则遍历结果是1234

Iterator 和 ListIterator 有什么区别?

  • 遍历。使用Iterator,可以遍历所有集合,如Map,List,Set;但只能在向前方向上遍历集合中的元素。
    使用ListIterator,只能遍历List实现的对象,但可以向前和向后遍历集合中的元素。
  • 添加元素。Iterator无法向集合中添加元素;而,ListIteror可以向集合添加元素。
  • 修改元素。Iterator无法修改集合中的元素;而,ListIterator可以使用set()修改集合中的元素。
  • 索引。Iterator无法获取集合中元素的索引;而,使用ListIterator,可以获取集合中元素的索引。

快速失败(fail-fast)和安全失败(fail-safe)

快速失败(fail—fast)

在用迭代器遍历一个集合对象时,如果遍历过程中集合对象对其内容进行了修改(增加、删除、修改),则会抛出ConcurrentModificationException

  • 原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。
  • 注意:这里异常的抛出条件是检测到 modCount != expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
  • 场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改),比如HashMap、ArrayList 这些集合类。

安全失败(fail—safe)

采用安全失败机制的集合容器,在遍历时,集合对象对其内容进行修改不会抛出异常

  • 原理:
  • 注意:对于当前next()指向元素的前面元素进行修改时,遍历过程中不会显示,后面修改则会显示;
  • 场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改,比如:ConcurrentHashMap。

foreach 与迭代器有什么关系?

foreach循环实际上使用的是迭代器,因此也遵循fail-fast和fail-safe机制

List

Arraylist与 LinkedList 异同点?

  • 线程安全: ArrayListLinkedList 都是线程不安全的;
  • 底层数据结构: Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向链表;
  • 插入和删除是否受元素位置的影响: ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。
  • 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而ArrayList 实现了RandomAccess 接口,所以有随机访问功能。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。
  • 内存空间占用: ArrayList的空间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

ArrayList 与 Vector 区别?

  • 线程安全:Vector是线程安全的,ArrayList不是线程安全的。其中,Vector在关键性的方法前面都加了synchronized关键字,来保证线程的安全性。如果有多个线程会访问到集合,那最好是使用 Vector,因为不需要我们自己再去考虑和编写线程安全的代码。
  • ArrayList在底层数组不够用时在原来的基础上扩展0.5倍,是原来的1.5倍, Vector扩展1倍,是原来的2倍,这样ArrayList就较为节约内存空间。

ArrayList 的扩容机制?

在第一次调用add方法时,会扩容为默认大小10;之后每次调用add方法都会先判断是否超出数组长度,如果超出则进行扩容

ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。默认情况下,新的容量会是原容量的1.5倍

Array 和 ArrayList 有什么区别?什么时候该用 Array 而不是 ArrayList 呢?

  • Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。
  • Array 大小是固定的,ArrayList 的大小是动态变化的。
  • ArrayList 提供了更多的方法和特性,比如:addAll(),removeAll(),iterator() 等等

Queue

BlockingQueue是什么?

Java.util.concurrent.BlockingQueue是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。

BlockingQueue接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue的实现类中被处理了。

Java提供了集中BlockingQueue的实现,比如ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue,、SynchronousQueue等。

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

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

HashMap

存储结构

HashMap 的底层数据结构是什么?

JDK1.8之前:数组+链表;数组为主体,链表来解决哈希冲突——发生哈希冲突后,使用头插法将冲突值放入链表头部
JDK1.8之后:Node数组+链表+红黑树实现,当链表长度超过阈值(8)且数组长度大于等于64时,将链表(查询时间复杂度为O(n))转换为红黑树(时间复杂度为O(log n)),极大的提高了查询效率

为什么在解决 hash 冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?

红黑树在插入和删除元素时需要进行左旋,右旋,变色等操作来保持平衡,效率是不如单链表的。因此在元素小于8个的时候做查询链表结构已经能保证查询性能。当元素大于 8 个的时候, 红黑树搜索时间复杂度是 O(logn),而链表是 O(n),此时需要红黑树来加快查询速度,但是新增节点的效率变慢了。

不用红黑树,用二叉查找树可以么?

可以。但是二叉查找树在极端情况下会变成一条链表, 遍历查找会非常慢。

当链表转为红黑树后,什么时候退化为链表?

为6的时候退转为链表。中间有个值7可以防止链表和树之间频繁的转换

为什么链表改为红黑树的阈值是 8?

泊松分布。
理想情况下使用随机的哈希码,容器中节点分布在 hash 桶中的频率遵循泊松分布,按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为 8 时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了 8,是根据概率统计而选择的。

默认加载因子是多少?为什么是 0.75,不是 0.6 或者 0.8 ?

0.75
负载因子 = 存储总元素个数 / 数组容量
0.75是空间成本和时间成本的一个折中,过大则哈希冲突变多,时间复杂度提高,空间利用率降低;过小则时间复杂度降低,空间利用率提高

索引计算

HashMap 中 key 的存储索引是怎么计算的?

  1. 取key的 hashCode 值
  2. 根据 hashcode 计算出hash值
  3. 通过取模计算下标

对于第2步,JDK1.7和JDK1.8也存在不同
JDK1.7:扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算
JDK1.8:扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算,hashcode值的高16位与低16位进行异或,源码如下

static final int hash(Object key) {
    int h;
    //hashcode值的高16位与低16位进行异或
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

JDK1.8 为什么要 hashcode 异或其右移十六位的值?

JDK 1.7 中扰动了 9 次,计算 hash 值的性能会稍差;而JDK1.8只有2次,速度更快,在数组 table 的 length 比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。

为什么 hash 值要与length-1相与?

把 hash 值对数组长度取模运算,模运算的消耗很大,没有位运算快。
当 length 总是 2 的n次方时,hash& (length-1) 运算等价于对length取模,也就是 hash%length,但是 & 比 % 具有更高的效率。

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

  1. 计算hash值在数组中的位置时,hash& (length-1) 运算等价于对length取模,在速度、效率上比直接取模要快得多
  2. length-1的所有位都是1, 假设为1111,那么hash的后四位就是应当放置的位置,只要hash足够均匀,那么位置就是均匀的;如果不是1111,那么一定有一位是0,和hash做与运算时一定有一位是0,不均匀
  3. 扩容时,便于计算新位置。假设之前容量为16, length-1 = 1111, 扩容一次后就是11111,如果hash的倒数第5位是1,那么现位置就是原位置+16,0即是原位置

当用户传入容量为任意值时,会自动转化为最近的一个大该值的2 的幂次方

put方法

HashMap 的put方法流程?

  1. 判断键值对数组table[ ]是否为空或为null,否则执行resize()进行扩容;
  2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点插入,转向⑥,如果table[i]不为空,转向③;
  3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode()以及equals();
  4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
  5. 遍历table[i],遍历过程中若发现key已经存在直接覆盖value即可;判断插入后的链表长度是否大于等于8,大于等于8的话判断tab数组长度是否大于等于64,小于则扩容,大于等于则把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;
  6. 插入成功后,判断实际存在的键值对数量size是否超过了阈值容量threshold,如果超过,进行扩容。

JDK1.7 和1.8 的put方法区别是什么?

  • 解决哈希冲突:只有链表; 链表+红黑树
  • 插入元素:JDK1.7 使用头插法插入元素,在多线程的环境下有可能导致环形链表的出现,扩容的时候会导致死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了,但JDK1.8 的 HashMap 仍然是线程不安全的

扩容机制

HashMap 的扩容方式?

Hashmap 在容量超过负载因子所定义的容量之后,就会扩容。Java 里的数组是无法自动扩容的,方法是将 Hashmap 的大小扩大为原来数组的两倍,并将原来的对象放入新的数组中。
JDK1.7 :直接将原数组里的元素重新计算在新数组中的位置
JDK1.8:看原来的 hash 值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引 + oldCap ”。这个设计非常的巧妙,省去了重新计算 hash 值的时间。

其他

HashMap的7种遍历方式

遍历的结果与插入顺序无关,是按照tab数组中key的存储顺序来进行遍历的

  • 使用迭代器(Iterator)EntrySet 的方式进行遍历;
  • 使用迭代器(Iterator)KeySet 的方式进行遍历;
  • 使用 For Each EntrySet 的方式进行遍历;
  • 使用 For Each KeySet 的方式进行遍历;
  • 使用 Lambda 表达式的方式进行遍历;
  • 使用 Streams API 单线程的方式进行遍历;
  • 使用 Streams API 多线程的方式进行遍历。(不保证顺序)

因为 parallelStream 为多线程版本性能一定是最好的
两个 entrySet 的性能相近,并且执行速度最快,接下来是stream ,然后是两个 keySet,性能最差的是 KeySet

EntrySet 之所以比 KeySet 的性能高是因为,KeySet 在循环时使用了 map.get(key),而 map.get(key) 相当于又遍历了一遍 Map 集合去查询 key 所对应的值。为什么要用“又”这个词?那是因为在使用迭代器或者 for 循环时,其实已经遍历了一遍 Map 集合了,因此再使用 map.get(key) 查询时,相当于遍历了两遍。

而 EntrySet 只遍历了一遍 Map 集合,之后通过代码“Entry<Integer, String> entry = iterator.next()”把对象的 key 和 value 值都放入到了 Entry 对象中,因此再获取 key 和 value 值时就无需再遍历 Map 集合,只需要从 Entry 对象中取值就可以了。

所以,EntrySet 的性能比 KeySet 的性能高出了一倍,因为 KeySet 相当于循环了两遍 Map 集合,而 EntrySet 只循环了一遍。

安全性

我们不能在遍历中使用集合 map.remove() 来删除数据,这是非安全的操作方式,但我们可以使用迭代器的 iterator.remove() 的方法来删除数据,这是安全的删除集合的方式。同样的我们也可以使用 Lambda 中的 removeIf 来提前删除数据,或者是使用 Stream 中的 filter 过滤掉要删除的数据进行循环,这样都是安全的,当然我们也可以在 for 循环前删除数据在遍历也是线程安全的。

//HashMap的四种遍历方式
public static void main(String[] args) {
    Map<String, String> map = new HashMap<String, String>();
    map.put("1", "value1");
    map.put("2", "value2");
    map.put("3", "value3");
    map.put("4", "value4");

    
    //第1种   通过Map.entrySet使用iterator遍历
    	Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<Integer, String> entry = iterator.next();
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }

    //第2种   通过Map.keySet使用iterator遍历
    	Iterator<Integer> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            Integer key = iterator.next();
            System.out.println(key);
            System.out.println(map.get(key));
        }
        
	//第3种   通过Map.entrySet()使用foreach遍历
	 	for (Map.Entry<Integer, String> entry : map.entrySet()) {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        }
        
    //第4种   通过Map.keySet使用foreach遍历
		for (Integer key : map.keySet()) {
            System.out.println(key);
            System.out.println(map.get(key));
        }
        
     //第5种   通过Lamda
    	map.forEach((key, value) -> {
            System.out.println(key);
            System.out.println(value);
        });

	//第6种  通过Streams API 单线程
		map.entrySet().stream().forEach((entry) -> {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        });
        
	//第7种  通过Streams API 多线程,不保证顺序
		map.entrySet().parallelStream().forEach((entry) -> {
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        });

}

还知道哪些hash算法?

Hash函数是指把一个大范围映射到一个小范围,目的往往是为了节省空间,使得数据容易保存。 比较出名的有MurmurHash、MD4、MD5等等

解决hash冲突的办法有哪些?HashMap用的哪种?

解决Hash冲突方法有:开放定址法(将计算得到的hash再次进行计算)、再哈希法(提供多个hash函数)、拉链法(建立链表)、建立公共溢出区(冲突的放到溢出区)。HashMap中采用的是拉链法 。

key 可以为 Null 吗?

可以,key 为 Null 的时候,hash算法最后的值以0来计算,也就是放在数组的第一个位置。

一般用什么作为HashMap的key?

一般用Integer、String 这种不可变类当 HashMap 当 key,而且 String 最为常用。

  • 字符串是不可变的,所以在它创建的时候 hashcode 就被缓存了,不需要重新计算。这就是 HashMap 中的键往往都使用字符串的原因。
  • 获取对象的时候要用到 equals() 和 hashCode() 方法,那么键对象正确的重写这两个方法是非常重要的,这些类已经很规范的重写了 hashCode() 以及 equals() 方法。

用可变类当 HashMap 的 key 有什么问题?

hashcode 可能发生改变,导致 put 进去的值,无法 get 出

HashMap为什么线程不安全?

  • 多线程下链表死循环。只在JDK1.8之前出现,JDK1.7中的 HashMap 使用头插法插入元素,在多线程的环境下,扩容的时候有可能导致环形链表的出现,形成死循环。因此,JDK1.8使用尾插法插入元素,在扩容时会保持链表元素原本的顺序,不会出现环形链表的问题。
  • 多线程的put可能导致元素的丢失。多线程同时执行 put 操作,如果计算出来的索引位置是相同的且该位置没有元素,那会造成前一个 key 被后一个 key 覆盖,从而导致元素的丢失。此问题在JDK 1.7和 JDK 1.8 中都存在。
  • put和get并发时,可能导致get为null。线程1执行put时,因为元素个数超出threshold而导致rehash,线程2此时执行get,而元素可能已经换了位置。此问题在JDK 1.7和 JDK 1.8 中都存在。
  • 多线程下两个线程一起扩容,那么这两个线程会各自扩容,rehash后赋给该map的底层数组,最后只有一个新数组有效,其他均会丢失

HashMap 与 HashTable 有什么区别?

  • 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(保证线程安全建议使用 ConcurrentHashMap );
  • 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;
  • 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
  • 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小。
  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
  • 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。

HashMap 和 ConcurrentHashMap 的区别

  • ConcurrentHashMap是线程安全的,使用了Node + CAS + Synchronized来保证并发安全
  • HashMap的键值对允许有null,但是ConcurrentHashMap键值都不允许为null

ConcurrentHashMap 和 HashTable 的区别?

  • 底层数据结构:ConcurrentHashMap和HashMap底层数据结构一致,都是数组+链表+红黑树,HashTable是数组+链表
  • 实现线程安全的方式:ConcurrentHashMap 使用Node + CAS + Synchronized来保证并发安全;HashTable使用 synchronized全表锁 来保证线程安全

HashSet 和 HashMap 区别?

HashSet底层是由HashMap实现的,相当于value 为object对象

HashMapHashSet
实现了Map接口实现Set接口
存储键值对仅存储对象
调用put()向map中添加元素调用add()方法向Set中添加元素
HashMap使用键(Key)计算HashcodeHashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
HashMap相对于HashSet较快,因为它是使用唯一的键获取对象HashSet较HashMap来说比较慢

ConcurrentHashMap

ConcurrentHashMap 的实现原理是什么

JDK1.7
JDK1.7中的 ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即ConcurrentHashMap 把哈希桶切分成小数组(Segment ),每个小数组有 n 个 HashEntry 组成。

其中,Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色;HashEntry 用于存储键值对数据。

在这里插入图片描述
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问,能够实现真正的并发访问。

JDK1.8
在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加低粒度的锁。CAS无锁操作本身效率很高,synchronzied则是进行了优化,锁升级的过程,因此比ReentrantLock在某些情况下效率更高

将锁的级别控制在了更细粒度的哈希桶元素级别,也就是说只需要锁住这个链表头结点(红黑树的根节点),就不会影响其他的哈希桶元素的读写,大大提高了并发度。
在这里插入图片描述

ConcurrentHashMap 的 put 方法执行逻辑是什么?

JDK1.7

首先,会尝试获取 Segment 锁,如果获取失败,利用自旋获取锁;如果自旋重试的次数超过 64 次,则改为阻塞获取锁。

获取到锁后:

  • 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
  • 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
  • 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
  • 释放 Segment 的锁。

JDK1.8

  • 根据 key 计算出 hash值。
  • 判断是否需要进行初始化。
  • 定位到 Node,拿到首节点 f,判断首节点 f:
  • 如果为 null ,则通过cas的方式尝试添加。
  • 如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容。
  • 如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入。
  • 当在链表长度达到8的时候,数组扩容(数据总量小于64)或者将链表转换为红黑树(数据总量大于等于64)。

ConcurrentHashMap 的 get 方法是否要加锁,为什么?

get 方法不需要加锁。因为 Node 的元素 val 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。
这也是它比其他并发集合比如 Hashtable、用 Collections.synchronizedMap()包装的 HashMap 安全效率高的原因之一。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    //可以看到这些都用了volatile修饰
    volatile V val;
    volatile Node<K,V> next;
}

get方法不需要加锁与volatile修饰的哈希桶有关吗?

没有关系。哈希桶table用volatile修饰主要是保证在数组扩容的时候保证可见性。

ConcurrentHashMap 不支持 key 或者 value 为 null 的原因?

不支持 value 为 null:因为ConcurrentHashMap是用于多线程的 ,如果map.get(key)得到了 null ,无法判断,是映射的value是 null ,还是没有找到对应的key而为 null ,这就有了二义性。
不支持 key 为 null:作者是这么觉得集合容器不应该有null

ConcurrentHashMap的size()方法是线程安全的吗

  • 1.7:先使用不加锁的方式计算size(),先计算一次,再计算一次,最多三次。如果两次结果相同,直接返回,如果不同,对每个Segment都加锁,再计算
  • 1.8:维护一个变量,表示size的值,使用CAS进行改变

ConcurrentHashMap 的并发度是多少?

ConcurrentHashMap 把实际 map 划分成若干部分来实现它的可扩展性和线程安全。这种划分是使用并发度获得的

在JDK1.7中,并发度默认是16,这个值可以在构造函数中设置。如果自己设置了并发度,ConcurrentHashMap 会使用大于等于该值的最小的2的幂指数作为实际并发度,也就是比如你设置的值是17,那么实际并发度是32。

ConcurrentHashMap 迭代器是强一致性还是弱一致性?

与HashMap迭代器是强一致性不同,ConcurrentHashMap 迭代器是弱一致性。

ConcurrentHashMap 的迭代器创建后,就会按照哈希表结构遍历每个元素,但在遍历过程中,内部元素可能会发生变化,如果变化发生在已遍历过的部分,迭代器就不会反映出来,而如果变化发生在未遍历过的部分,迭代器就会发现并反映出来,这就是弱一致性。

这样迭代器线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键

JDK1.7与JDK1.8 中ConcurrentHashMap 的区别?

  • 数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
  • 保证线程安全机制:JDK1.7采用Segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8 采用CAS+Synchronized保证线程安全。
  • 锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
  • 查询时间复杂度:从原来的遍历链表O(n),可能变成遍历红黑树O(logN)

ConcurrentHashMap 和Hashtable的效率哪个更高?为什么?

ConcurrentHashMap 的效率要高于Hashtable,因为Hashtable给整个哈希表加了一把大锁从而实现线程安全。而ConcurrentHashMap 的锁粒度更低,在JDK1.7中采用分段锁实现线程安全,在JDK1.8 中采用CAS+Synchronized实现线程安全。

Hashtable的锁机制 ?

Hashtable是使用Synchronized来实现线程安全的,给整个哈希表加了一把全表锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞等待需要的锁被释放,在竞争激烈的多线程场景中性能就会非常差

多线程下安全的操作 map还有其他方法吗?

Collections.synchronizedMap()方法,对方法进行加同步锁, 本质也是对 HashMap 进行全表锁

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值