详细解读Java中Map集合的底层原理(干货+源码解读)

本文将为大家详细讲解Java中的Map集合,这是我们进行开发时经常用到的知识点,也是大家在学习Java中很重要的一个知识点,更是我们在面试时有可能会问到的问题。

文章较长,干货满满,建议大家收藏慢慢学习。文末有本文重点总结,主页有全系列文章分享。技术类问题,欢迎大家和我们一起交流讨论!

前言

在上一篇文章中给大家讲解了Java里的Set集合及其常用子类。现在我们已经掌握了Java里的两大集合,最后还有另一大集合等待着我们学习,这就是Map集合。与之前的集合不太一样,Map集合属于双列集合,该集合中的信息是key-value形式;而之前的LIst和Set都是单列集合,里面的元素没有key。

有些小伙伴可能会很好奇,我们已经学习了List和Set集合了,为什么还要再搞出来一个Map集合呢?Map集合与Collection集合又有什么不同呢? 要想搞清楚以上问题,我们可以考虑这么一个需求。

假如我们利用List集合,来存储一份Student学生信息,要求你根据name来查找某个指定的学生分数,你要怎么实现?按照我们之前的经验,常用的方式就是遍历这个List,并挨个判断name是否相等,找到后就返回指定元素。

其实这种需求非常的常见,通过一个关键词来查询对应的值。如果我们使用List来实现该需求,效率会非常的低,因为平均需要扫描完集合的一半元素才能确定。而如果使用Map这种key-value键值对映射表的数据结构,就可以通过key快速地查找到value值。


全文大约【8500】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......

一. Map集合

1. 简介

Map集合是一种以键值对形式存储和操作数据的数据结构,建立了key-value之间的映射关系,常用于存储和处理复杂的数据。同时Map也是一种双列集合接口,它有多个实现类,包括HashMap、TreeMap、LinkedHashMap等,最常用的是HashMap类。其中,HashMap是按哈希算法来实现存取键对象的,这是我们开发时最常用的Map子类,而TreeMap类则可以对键对象进行排序。

image.png

Map集合中的每个元素,都包含了一个键(key)和一个值(value),key和value组成了键-值的映射表,我们称其为键值对。键用于唯一标识一个元素,值用于存储该元素的数据,一般情况下,这个key和value可以是任何引用类型的数据。其中,键key是无序、无下标、不重复的,最多只能有一个key为null。值value是无序、无下标、可重复的,可以有多个value为null

并且这个key和value之间是单向的一对一关系,即通过指定的key,总能找到唯一的、确定的value。当我们想从Map中取出数据时,只要给出指定的key,就能取出对应的value。

配套视频如下 :戳我直达视频

2. 特点

根据上面我们对Map概念的讲解,把Map的主要特点给大家总结如下:

  • Map List 不同,Map是一种双列集合;
  • Map 存储的是 key-value 的映射关系;
  • Map 不保证顺序 。在遍历时,遍历的顺序不一定是 put() 时放入的 key 的顺序,也不一定是 key 的排序顺序。

3. 实现方式

在Java中,Map集合的实现方式主要有两种:基于哈希表和基于树结构。接下来给大家简单介绍一下基于这两种结构的Map集合。

3.1 基于哈希表的Map集合

基于哈希表的Map,其底层是基于哈希表作为数据结构的集合,主要的实现类是HashMap

HashMap中,每个元素都包含一个键和一个值。当我们在添加元素时,HashMap会根据键的哈希值计算出对应的桶(Bucket),并将元素存储到该桶中。如果不同的键具有相同的哈希值,就会出现哈希冲突,此时HashMap会使用链表或红黑树等数据结构来解决冲突。基于哈希表的Map集合具有快速的添加、删除和查找元素的特点,但元素的顺序是不确定的。

3. 实现方式

在Java中,Map集合的实现方式主要有两种:基于哈希表和基于树结构。接下来壹哥给大家简单介绍一下基于这两种结构的Map集合。

3.1 基于哈希表的Map集合

基于哈希表的Map,其底层是基于哈希表作为数据结构的集合,主要的实现类是HashMap。在HashMap中,每个元素都包含一个键和一个值。当我们在添加元素时,HashMap会根据键的哈希值计算出对应的桶(Bucket),并将元素存储到该桶中。如果不同的键具有相同的哈希值,就会出现哈希冲突,此时HashMap会使用链表或红黑树等数据结构来解决冲突。基于哈希表的Map集合具有快速的添加、删除和查找元素的特点,但元素的顺序是不确定的。

3.2 基于树结构的Map集合

基于树结构的Map集合,其底层是基于红黑树作为数据结构的集合,主要的实现类是TreeMap。在TreeMap中,每个元素也都包含一个键和一个值。我们在添加元素时,TreeMap会根据键的比较结果,将元素存储到正确的位置上,使得元素可以按照键的升序或降序排列。基于树结构的Map集合,具有快速查找和遍历元素的特点,但添加和删除元素的速度较慢。

4. 常用方法

Map集合的使用和其他集合类似,主要包括添加、删除、获取、遍历元素等操作。

当我们调用put(K key, V value)方法时,会把key和value进行映射并放入Map。当调用V get(K key)时,可以通过key获取到对应的value;如果key不存在,则返回null。如果我们只是想查询某个key是否存在,可以调用containsKey(K key)方法。另外我们也可以通过 Map提供的keySet()方法,获取所有key组成的集合,进而遍历Map中所有的key-value对。

下表中就是Map里的一些常用方法,大家可以记一下,这些方法在Map的各实现子类中也都存在。

方法 描述
clear() 删除Map中所有的键-值对
isEmpty() 判断Map是否为空
size() 计算Map中键/值对的数量
put() 将键/值对添加到Map中
putAll() 将所有的键/值对添加到Map中
putIfAbsent() 如果Map中不存在指定的键,则将指定的键/值对插入到Map中。
remove() 删除Map中指定键key的映射关系
containsKey() 检查Map中是否存在指定key对应的映射关系。
containsValue() 检查Map中是否存在指定的 value对应的映射关系。
replace() 替换Map中指定key对应的value。
replaceAll() 将Map中所有的映射关系替换成给定函数执行的结果。
get() 获取指定 key 对应对 value
getOrDefault() 获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值
forEach() 对Map中的每个映射执行指定的操作。
entrySet() 返回Map中所有的键值对
keySet() 返回Map中所有的key键
values() 返回Map中所有的value值
merge() 添加键值对到Map中
compute() 对Map中指定key的值进行重新计算
computeIfAbsent() 对Map中指定key的值进行重新计算,如果该key不存在,则添加到Map中
computeIfPresent() 当key存在该Map时,对Map中指定key的值进行重新计算

与本节内容配套的视频链接如下: 戳我直达视频课程

5. 常用实现类

Java中有多个Map接口的实现类,接下来就给大家逐一简单介绍一下。

image.png

5.1 HashMap

HashMap是Java中最常用的Map集合实现类,它基于哈希表实现,具有快速查找键值对的优点。 HashMap的存储方式是无序的,也就是说,遍历HashMap集合时,得到的键值对顺序是不确定的。下面是创建HashMap集合的代码示例:

Map<String, Integer> hashMap = new HashMap<>();

5.2 TreeMap

TreeMap是Java中另一个常用的Map集合实现类,它基于红黑树实现,具有自动排序键值对的优点。TreeMap的存储方式是有序的,也就是说,遍历TreeMap集合时得到的键值对,是按照键的自然顺序或指定比较器的顺序排序的。下面是创建TreeMap集合的代码示例:

Map<String, Integer> treeMap = new TreeMap<>();

5.3 LinkedHashMap

LinkedHashMap是Java中另一个Map集合实现类,它继承自HashMap,并保持了插入顺序。也就是说,遍历LinkedHashMap集合时,得到的键值对的顺序是按照插入顺序排序的。下面是创建LinkedHashMap集合的代码示例:

Map<String, Integer> linkedHashMap = new LinkedHashMap<>();

5.4 Hashtable

Hashtable是Java中另一个Map集合实现类,它与HashMap非常相似,但Hashtable是线程安全的。Hashtable的存储方式是无序的,也就是说,遍历Hashtable集合时,得到的键值对的顺序是不确定的。下面是创建Hashtable集合的代码示例:

Map<String, Integer> hashtable = new Hashtable<>();

需要注意的是,由于Hashtable是线程安全的,所以在多线程环境中使用Hashtable的性能可能会较低,现在一般是建议使用ConcurrentHashMap。

5.5 ConcurrentHashMap

ConcurrentHashMap是Java中的另一个Map集合实现类,它与Hashtable非常相似,但是ConcurrentHashMap是线程安全的,并且性能更高。ConcurrentHashMap的存储方式是无序的,也就是说,遍历ConcurrentHashMap集合时,得到的键值对的顺序是不确定的。下面是创建ConcurrentHashMap集合的代码示例:

Map<String, Integer> concurrentHashMap = new ConcurrentHashMap<>();

需要注意的是,虽然ConcurrentHashMap是线程安全的,但仍然需要注意多线程环境下的并发问题

二. HashMap集合

1. 简介

HashMap继承自AbstractMap类,实现了Map、Cloneable、java.io.Serializable接口,如下图所示:

image.png

HashMap是基于散列表实现的双列集合,它存储的是key-value键值对映射,每个key-value键值对也被称为一条Entry条目。其中的 key与 value,可以是任意的数据类型,其类型可以相同也可以不同。但一般情况下,key都是String类型,有时候也可以使用Integer类型;value可以是任何类型。并且在HashMap中,最多只能有一个记录的key为null,但可以有多个value的值为null。

HashMap中这些键值对(Entry)会分散存储在一个数组当中,这个数组就是HashMap的主体。 默认情况下,HashMap这个数组中的每个元素的初始值都是null。但HashMap中最多只允许一条记录的key为null,允许多条记录的value为null。

另外HashMap是非线程安全的,即任一时刻如果有多个线程同时对HashMap进行写操作,有可能会导致数据的不一致。 如果需要满足线程的安全性要求,可以用 Collections.synchronizedMap() 方法使得HashMap具有线程安全的能力,或者使用ConcurrentHashMap来替代。

HashMap实现了Map接口,会根据key的hashCode值存储数据,具有较快的访问速度。另外HashMap是无序的,不会记录插入元素的顺序。我们一般是根据key的hashCode值来存取数据, 大多数情况下都可以直接根据key来定位到它所对应的值,因而HashMap有较快的访问速度,但遍历顺序却不确定。

2. 特点

根据上面的描述,把HashMap的核心特点总结如下:

  • 基于哈希表实现,具有快速查找键值对的优点;
  • HashMap的存储方式是无序的;
  • HashMap的key-value可以是任意类型,但key一般都是String类型,value类型任意;
  • HashMap最多只能有一个记录的key为null,但可以有多个value为null。

3. 常用操作

HashMap的使用方法和其他Map集合类似,主要包括添加元素、删除元素、获取元素、遍历元素等操作。接下来会详细地给大家介绍HashMap集合的常用操作。

3.1 创建Map集合对象

创建HashMap对象的方式有多种,我们可以使用构造函数,代码如下:

// 使用构造函数创建HashMap对象 
Map<String, Integer> map1 = new HashMap<>();

3.2 添加元素

向HashMap集合添加元素的方式和List集合类似,也是使用put()方法,下面是向HashMap集合中添加元素的代码示例:

public class Demo14 {

	public static void main(String[] args) {
		//HashMap
		Map<String, String> map = new HashMap<>();
		map.put("name","一一哥");
        //map.put("name","壹哥");
		map.put("age", "30");
		map.put("sex", "男");
		System.out.println("map="+map);
	}
}

看到上面的代码,有些小伙伴可能会问,如果我们往同一个Map中存储两个相同的key,但分别放入不同的value,这会有什么问题吗?

其实我们在Map中,重复地放入 key-value 并不会有任何问题,但一个 key 只能关联一个 value 因为当我们调用put(K key, V value)方法时,如果放入的key已经存在,put()方法会返回被删除的旧value,否则就会返回null。所以Map中不会有重复的key,因为放入相同的key时,就会把原有key对应的value给替换掉。

3.3 删除元素

从HashMap集合中删除元素的方式也和List集合类似,使用remove()方法。下面是从HashMap集合中删除元素的代码示例:

//从HashMap集合中移除元素
map.remove("age");

3.4 获取元素

从HashMap集合中获取元素的方式和List集合不同,需要使用键来获取值。HashMap集合提供了两种方法来获取值,一种是使用get()方法,另一种是使用getOrDefault()方法。如果指定的键不存在,使用get()方法会返回null,而getOrDefault()方法则会返回指定的默认值。下面是从HashMap集合中获取元素的代码示例:

public class Demo15 {

	public static void main(String[] args) {
		//HashMap
		Map<String, String> map = new HashMap<>();
		map.put("name","一一哥");
		map.put("age", "30");
		map.put("sex", "男");
		
		//根据key获取指定的元素
		String name = map.get("name");
		String age = map.get("age");
		//根据key获取指定的元素,并设置默认值
		String sex = map.getOrDefault("sex","男");
		String height = map.getOrDefault("height","0");
		System.out.println("[name]="+name+",[age]="+age+",[sex]="+sex+",[height]="+height);
	}
}

3.5 遍历元素

遍历HashMap集合的方式和List集合不同,需要使用迭代器或者foreach循环遍历键值对,下面是遍历HashMap集合的代码示例:

/**
 * @author 一一哥Sun
 */
public class Demo16 {

	public static void main(String[] args) {
		//HashMap
		Map<String, String> map = new HashMap<>();
		map.put("name","一一哥");
		map.put("age", "30");
		map.put("sex", "男");
		
		//遍历方式一:使用迭代器遍历HashMap 
		//获取集合中的entry条目集合
		Set<Entry<String, String>> entrySet = map.entrySet();
		//获取集合中携带的Iterator迭代器对象
		Iterator<Map.Entry<String, String>> iterator = entrySet.iterator(); 
		//通过循环进行迭代遍历
		while (iterator.hasNext()) {     
			//获取每一个Entry条目对象
		    Map.Entry<String, String> entry = iterator.next();    
		    //获取条目中的key
		    String key = entry.getKey();    
		    //获取条目中的value
		    String value = entry.getValue();     
		    System.out.println(key + " = " + value); 
		} 

		//遍历方式二:用foreach循环遍历HashMap 
		for (Map.Entry<String, String> entry : map.entrySet()) {    
		    String key = entry.getKey();     
		    String value = entry.getValue();     
		    System.out.println(key + " = " + value); 
		}
	}
}

大家要注意,当我们使用Map时,任何依赖顺序的逻辑都是不可靠的。比如,我们存入"A","B","C" 3个key,遍历时,每个key会保证被遍历一次且仅遍历一次,但遍历的顺序完全没有保证,甚至对于不同的JDK版本,相同的代码遍历输出的顺序都可能是不同的!所以我们在 遍历Map时,要注意输出的key是无序的!

3.6 判断Map集合是否为空

判断HashMap集合是否为空可以使用isEmpty()方法,如果Map集合为空,则返回true,否则返回false。下面是判断HashMap集合是否为空的代码示例:

// 判断HashMap是否为空 
boolean isEmpty = map.isEmpty(); 

3.7 获取Map集合的大小

获取HashMap集合的大小可以使用size()方法,返回HashMap集合中键值对的数量,下面是获取HashMap集合大小的代码示例:

// 获取HashMap的大小 
int size = map.size(); 

3.8 判断Map集合是否包含指定的键或值

如果我们想判断HashMap集合是否包含指定的键或值,可以使用containsKey()和containsValue()方法。如果Map集合包含指定的键或值,则返回true,否则返回false。下面是判断HashMap集合是否包含指定键或值的代码示例:

// 判断HashMap是否包含指定键
boolean containsKey = map.containsKey("name"); 

// 判断HashMap是否包含指定值 
boolean containsValue = map.containsValue("一一哥"); 

3.9 替换Map集合中的键值对

如果我们想替换HashMap集合中的键值对,可以使用replace()方法将指定键的值替换成新的值。如果指定的键不存在,则不进行任何操作。下面是替换HashMap集合中的键值对的代码示例:

// 替换HashMap中的键值对 
map.replace("name", "壹哥"); 

3.10 合并两个Map集合

如果我们想合并两个HashMap集合,可以使用putAll()方法,将另一个HashMap集合中所有的键值对,添加到当前的HashMap集合中。下面是合并两个HashMap集合的代码示例:

public class Demo16 {

	public static void main(String[] args) {
		//HashMap
		Map<String, String> map1 = new HashMap<>();
		map1.put("name","一一哥");
		map1.put("age", "30");
		map1.put("sex", "男");
		
		// 创建另一个TreeMap集合 
		Map<String, String> map2 = new HashMap<>(); 
		map2.put("height", "180"); 
		map2.put("salary", "5w"); 

		//将map1中的键值对添加到map2中 
		map2.putAll(map1); 
		System.out.println("map2="+map2);
	}
}

3.11 获取Map集合中所有的键或值

如果我们想获取HashMap集合中所有的键或值,可以分别使用keySet()和values()方法。这两个方法会返回一个Set集合和一个Collection集合,它们包含了HashMap集合中所有的键或值。下面是获取HashMap集合中所有键或值的代码示例:

public class Demo18 {

	public static void main(String[] args) {
		//HashMap
		Map<String, String> map = new HashMap<>();
		map.put("name","一一哥");
		map.put("age", "30");
		map.put("sex", "男");
		
		// 获取HashMap中所有的键 
		Set<String> keySet = map.keySet(); 
		for(String key : keySet) {
			System.out.println("key="+key); 
		}

		// 获取HashMap中所有的值 
		Collection<String> values = map.values(); 
		for(String value:values) {
			System.out.println("value"+value); 
		}
	}
}

4. 原理概述(重点)

作为开发时最常用的Map集合,HashMap在我们面试时被问到的概率非常高,尤其是关于其原理、数据结构、冲突解决、并发、扩容等相关的内容,更是经常被问到。

如果我们想要了解HashMap的底层原理,首先得知道HashMap的底层数据结构,而这个数据结构在不同的JDK版本中是不同的。我们可以把HashMap的底层数据结构分为两大版本,即JDK 7及其以前的版本 和 JDK 8及其以后的版本。 大家注意,本文主要是结合JDK 8的源码,给大家讲解HashMap的底层原理。

  • 在JDK 7及其以前版本的HashMap中,其底层的数据结构是 数组+链表
  • 而从JDK 8开始则采用 数组+链表+红黑树 的数据结构,其中的 数组是Entry类型或者说是Node类型数组

5. hash冲突解决机制

因为HashMap底层会使用Hash算法来处理数据的存取,当数据非常多时就有一定的概率出现hash冲突,其解决过程如下。

  • 当我们往HashMap中存储数据时,首先会利用hash(key)方法 计算出key的hash值,再利用该 hash值HashMap数组的长度-1 进行 与运算,从而得到该key在数组中对应的下标位置
  • 如果该位置上目前还没有存储数据,则直接将该key-value键值对数据存入到数组中的这个位置;
  • 如果该位置目前已经有了数据,则把新的数据存入到一个链表中;
  • 当链表的长度超过阈值(JDK 8中该值为8)时,会将链表转换为红黑树(转换为红黑树还需要满足其他的条件,链表长度达到阈值只是其中的一个条件)。

image.png

通过这样的机制,HashMap就解决了存值时可能产生的哈希冲突问题,并可以大大提高我们的查找效率。

当然由于HashMap极其重要,它的内容非常多,尤其是原理性的内容更多。但由于篇幅限制,不会在本文中过多地讲解HashMap原理等的内容

三. Hashtable集合

1. 简介

Hashtable也是Java中的一个Map集合,它与HashMap非常相似,但Hashtable是线程安全的,而HashMap不是线程安全的。Hashtable也可以存储键值对,并可以通过键快速查找到对应的值,Hashtable的键和值也都可以是任何类型的对象。

因为Hashtable是线程安全的,因此适用于多线程环境。在多线程环境中,如果需要对Hashtable进行多个操作,需要使用synchronized关键字来保证线程安全。但需要我们注意的是,在多线程环境中使用Hashtable可能会影响性能,所以如果不需要保证线程安全,请尽量使用HashMap。

Hashtable集合的底层结构主要是数组+链表,数组是Entry数组,链表也是用Entry来实现的 所以Hashtable的底层核心,其实也是基于哈希表,它使用哈希函数将键映射到哈希表中的一个位置,从而实现快速查找。另外Hashtable的存储方式是无序的,也就是说,遍历Hashtable集合得到的键值对的顺序是不确定的。

2. 常用方法

Hashtable与HashMap类似,它的常用方法也与HashMap一样:

  • put(K key, V value):将指定的键值对存储到Hashtable中。
  • get(Object key):返回指定键所对应的值,如果不存在该键则返回null。
  • remove(Object key):从Hashtable中移除指定键所对应的键值对。
  • containsKey(Object key):判断Hashtable中是否包含指定的键。
  • containsValue(Object value):判断Hashtable中是否包含指定的值。
  • size():返回Hashtable中键值对的数量。
  • clear():移除Hashtable中的所有键值对。

3. 基本使用

下面是一个简单的Hashtable集合示例,演示了如何创建Hashtable集合、存储键值对、获取值、遍历集合等操作。

import java.util.Hashtable;
import java.util.Map;

public class Demo19 {
    public static void main(String[] args) {
        // 创建Hashtable集合
        Map<String, Integer> hashtable = new Hashtable<>();

        // 存储键值对
        hashtable.put("apple", 10);
        hashtable.put("banana", 20);
        hashtable.put("orange", 30);

        // 获取值
        int value1 = hashtable.get("apple");
        int value2 = hashtable.get("banana");
        int value3 = hashtable.get("orange");
        System.out.println("apple: " + value1);
        System.out.println("banana: " + value2);
        System.out.println("orange: " + value3);

        // 移除键值对
        hashtable.remove("orange");

        // 遍历集合
        for (Map.Entry<String, Integer> entry : hashtable.entrySet()) {
            String key = entry.getKey();
            int value = entry.getValue();
            System.out.println(key + ": " + value);
        }

        // 清空集合
		hashtable.clear(); 
    }
}

其他方法的使用与HashMap基本一致,就不再一一细说了。

四. ConcurrentHashMap集合

1. 简介

由于在多线程环境下,常规的HashMap可能会在数组扩容及重哈希时出现 死循环、脏读 等线程安全问题,虽然有HashTable、Collections.synchronizedMap()可以取代HashMap进行并发操作,但因它们都是利用一个 全局的synchronized锁 来同步不同线程之间的并发访问,因此性能较差。

所以Java就从JDK 1.5版本开始,在J.U.C(java.util.concurrent并发包) 引入了一个高性能的并发操作集合---ConcurrentHashMap(可以简称为CHM)。该集合是一种线程安全的哈希表实现,相比Hashtable和SynchronizedMap,在多线程场景下具有更好的性能和可伸缩性

并且ConcurrentHashMap集合在读数据时不需要加锁,写数据时会加锁,但锁的粒度较小,不会对整个集合加锁。而其内部又大量的利用了 volatile,final,CAS等lock-free(无锁并发)技术,减少了锁竞争对于性能的影响,具有更好的写并发能力,但降低了对读一致性的要求。 因此既保证了并发操作的安全性,又确保了读、写操作的高性能,可以说它的并发设计与实现都非常的精巧。

另外ConurrentHashMap中的key与value都不能为null,否则会产生空指针异常!

2. ConcurrentHashMap类关系

ConcurrentHashMap与HashMap具有相同的父类AbstractMap,他俩可以说是“亲兄弟”,所以ConcurrentHashMap在一般的特性和使用上与HashMap基本是一致的,甚至很多底层原理也是相似的。

但两者所在的包是不同的,ConcurrentHashMap是在java.util.concurrent包中,HashMap是在java.util包中,我们可以参考下图:

image.png

以上所述的ConcurrentHashMap概念及特征,是不区分版本的,但实际上不同版本的ConcurrentHashMap内部实现差异很大,所以面试时经常会被问到不同版本之间的差异、各自特征。接下来会针对JDK 7 与 JDK 8 这两个经典版本分别进行阐述。

3. 实现原理

3.1 并发控制机制

在JDK 7中,ConcurrentHashMap的核心机制是分段锁(Segment),每个Segment内部维护了一个哈希表,且这个哈希表是线程安全的。而ConcurrentHashMap中的每个操作,都是先对所在的Segment进行加锁,然后再执行具体的操作。

当多个线程对不同的Segment进行操作时,它们之间是并发的。当多个线程对同一个Segment进行操作时,它们会竞争锁,但不会影响到其他Segment的操作。这种机制有效地降低了锁的粒度,提高了并发访问效率。

ConcurrentHashMap的另一个优点是支持可伸缩性。当需要增加ConcurrentHashMap的容量时,我们只需要增加Segment的数量即可,这种机制使得ConcurrentHashMap在高并发场景下具有良好的可伸缩性。

3.2 JDK 7版本的ConcurrentHashMap

在JDK 7版本中,ConcurrentHashMap和HashMap的设计思路其实是差不多的,但为了支持并发操作,做了一定的改进。比如ConcurrentHashMap中的数据是一段一段存放的,我们把这些分段称之为Segment分段,在每个Segment分段中又有 数组+链表 的数据结构。

默认情况下ConcurrentHashMap把主干分成了 16个Segment分段,并对每一段都单独加锁,我们把这种设计策略称之为 "分段锁"ConcurrentHashMap就是利用这种分段锁机制进行并发控制的。JDK 7中ConcurrentHashMap的基本数据结构如下图所示:

image.png

在理想状态下,ConcurrentHashMap可以 同时支持16个线程 执行并发写操作,以及任意数量的线程进行并发读操作。在写操作时,通过分段锁技术,只对所操作的段加锁而不会影响其它分段,且在读操作时(几乎)不需要加锁。

3.3 JDK 8版本的ConcurrentHashMap

在JDK 8中,ConcurrentHashMap相对于JDK 7版本做了很大的改动,从实现的代码量上就可以看出来,JDK 7中ConcurrentHashMap不到2000行代码,而JDK 8中则有6000多行代码。

JDK 8中放弃了臃肿的Segment设计,取而代之采用了 Node数组 + 链表 + 红黑树 + CAS + Synchronized 技术来保证并发安全,实现并发控制操作。但是在ConcurrentHashMap的实现中保留了 Segment 的定义,这是为了 保证序列化时的兼容性,但并没有任何结构上的用处。在JDK 8中用synchronized替换ReentrantLock的原因大致如下:

  • 减少内存开销: 如果使用ReentrantLock,就需要节点继承AQS来获得同步支持,这增加了内存开销,而JDK 8中只有头节点需要进行同步。
  • 内部优化: synchronized是JVM直接支持的,JVM能够在运行时作出相应的优化措施,比如 锁粗化、锁消除、锁自旋等。

4. 基本使用

ConcurrentHashMap支持与HashMap相同的常用操作,如put、get、remove等。下面介绍一些常用操作。

4.1 插入元素

ConcurrentHashMap的put方法用于向集合中插入一个键值对,如果键已经存在,则会更新对应的值。下面是向ConcurrentHashMap中插入元素的示例:

import java.util.concurrent.ConcurrentHashMap;

public class Demo19 {
public static void main(String\[] args) {
// 创建ConcurrentHashMap集合
ConcurrentHashMap\<String, Integer> map = new ConcurrentHashMap<>();

        // 插入元素
        map.put("apple", 10);
        map.put("banana", 20);
        map.put("orange", 30);

        // 输出元素
        System.out.println(map);
    }

}

根据上面代码的执行结果可知,ConcurrentHashMap中的键值对是无序的。

4.2 获取元素

ConcurrentHashMap的get方法用于获取指定键对应的值,如果键不存在,则返回null。下面是一个从ConcurrentHashMap中获取元素的示例:

import java.util.concurrent.ConcurrentHashMap;

public class Demo20 {
    public static void main(String[] args) {
        // 创建ConcurrentHashMap集合
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // 插入元素
        map.put("apple", 10);
        map.put("banana", 20);
        map.put("orange", 30);

        // 获取元素
        Integer value = map.get("apple");
        System.out.println(value);
    }
}

4.3 删除元素

ConcurrentHashMap的remove方法用于从集合中删除指定键的元素,下面是一个从ConcurrentHashMap中删除元素的示例:

import java.util.concurrent.ConcurrentHashMap;

public class Demo21 {
    public static void main(String[] args) {
        // 创建ConcurrentHashMap集合
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        // 插入元素
        map.put("apple", 10);
        map.put("banana", 20);
        map.put("orange", 30);

        // 删除元素
        map.remove("apple");

        // 输出元素
        System.out.println(map);
    }
}

五. 结语

至此,我们就把Map及其子类学习完了,现在你知道Map有什么特性及其用法了吗?接下来我们简单总结一下今天的重点吧:

  • Map是一种映射表,可以通过key快速查找value;
  • 可以通过 for each 遍历 keySet() ,也可以通过 for each 遍历 entrySet() ,直接获取 key-value
  • 最常用的Map是HashMap;
  • Hashtable是线程安全的集合,底层结构主要是数组+链表;
  • ConcurrentHashMap可以良好地进行并发处理,是一种线程安全且高效的集合。

本文转载于网络 如有侵权请联系删除

相关文章

  • 装逼shell 适合linux or mac book

    文章目录装逼shell装逼shellsl$sl -aAnaccidentseemstohappen.You'llfeelpityforpeoplewhocryforhelp. -lshowslittleone. -FItflies. -eAllowinterruptbyCtrl+C. $aliasls=sl复制fortune$fortune#诗歌复制cmatrix$cmatrix#黑客帝国复制figlettoilet命令$figletnihaoshijie $toilethelloworld复制yes$yes我很NB复制

  • CSS布局之道——内凹圆角

    一、效果图先来看一下效果图:二、实现1.场景看上图,此类场景应该很是常用吧,比如账单、卡片、列表等。2.实现思路看到效果图,能想到的实现方法则是planA:border-radius直接搞起来planB:定位但是细想之后发现两者皆不可用,border-radius处理的话是凸角,舍弃;定位需要独立出来两个模块,复杂度高了,舍弃。可是舍弃后,需要怎么做呢?切背景图吗?显然不合理,如果内容不固定,则模块的长度也不一样,背景图也会拉伸;思来想去,还是得从定位上做文章:既然元素多比较复杂,那就直接在伪类上动手。可能大家猜到了,就是直接在::before、::after上模拟出来。三、代码1.基础布局<!--html:随便一些div--> <div></div> <div></div> <div></div> <div></div> <div></div> 复制body{ margin:0; background:#000; padding:20px

  • pytorch基础知识-维度变换-(上)

    维度变换是pytorch中的重要操作,尤其是在图片处理中。本文对pytorch中的维度变换进行讲解。维度变换有四种操作:(1)view/reshape(类似于numpy中的reshape操作,可在不改变数据情况下,将tensor转换shape)(2)Squeeze/unsqueeze(前一个为删减维度,第二个为增加维度)(3)Transpose/t/permute(矩阵的单次、多次交换操作)(4)Expand/repeat(维度的扩展)下面分别介绍view和reshape操作View和reshape两者可实现相同的操作,他们之所以不同是因为他们分别出现在pytorch0.3和0.4版本中。下面以具体代码介绍其功能importtorch a=torch.rand(4,1,28,28) #创建4张图片,一个通道(黑白照片),28*28像素点的图片 print(a.shape) #查看a的shape b=a.view(4,28*28) #代表将后面28*28的像素点全合并在一起,这样变为[4,784]的shape,各个像素点失去了上下级的关系,全部被打平。这样处理的数据适合于全连接层的计

  • Git中的"pull request"真正比较的是什么?

    前言利用git版本控制工具时,我们通常会从主分支拉出新分支进行开发,开发完成后创建pr(也就是pullrequest),让其他小伙伴帮忙review,确定代码没有问题后再将新分支合并到主分支上。但是,你真的理解pullrequest中比较的两个分支到底是谁吗?下面以一个虚拟案例进行说明:假设主分支名为“Master”,拉出来的新分支名为“developBrance1”。注:图中的箭头指代工作推进方向,而不是提交的指向(提交指向总是由当前提交指向父提交,和这里的箭头是反着的)最简单的情况上图中,我们从主分支Master的m1提交点拉出新分支developBranch1,然后在developBranch1分支上开发(开发过程中产生了d1、d2、d3共3个提交),开发完成后创建pr,然后经过Review后将其合并到主分支上形成新的提交点N。自然而然地,我们创建pr时选择的源和目标为:src[developBranch1]->dest[Master]我们期望pr比较的是developBranch1和Master这两个分支的最新提交点,pr实际比较的也是developBranch1的d3提

  • GSEA结果解读

    1Enrichmentscore(ES)ES是GSEA最初的结果,反应全部杂交data排序后,在此序列top或bottom富集的程度。 ES原理:扫描排序序列,当出现一个功能集中的gene时,增加ES值,反之减少ES值,所以ES是个动态值。最终ES的确定是讲杂交数据排序序列所在位置定义为0,ES值定义为距离排序序列的最大偏差. ES为正,表示某一功能gene集富集在排序序列前方 ES为负,表示某一功能gene集富集在排序序列后方。 图中的最高点为此通路的ES值,中间表示杂交数据的排序序列。竖线表示此通路中出现的芯片数据集中的gene。2NES由于ES是根据分析的数据集中的gene是否在一个功能geneset中出现来计算的,但各个功能geneset中包含的gene数目不同,且不同功能geneset与data之间的相关性也不同,因此,比较dataset在不同功能geneset中的富集程度要对ES进行标准化处理,,也就是NES NES=某一功能geneset的ES/数据集所有随机组合得到的ES平均值 NES是主要的统计量。3FDRNES确定后,判断其中可能包含的错误阳性发现率。FDR=25%

  • 能从长相上看出性取向?这样的AI你怕不怕?

    导读:八卦,似乎一直是人类茶余饭后一个永恒的话题,怎么辨别一个人与另一个人的关系?比如,是好朋友还是好基友?然而,你兴许尚不知的是,这一私密的问题,AI已经能够做到精确识别了,这也着实引起了一拨恐慌。作者:柯鸣来源:智能相对论(ID:aixdlun)01同性恋倾向于具有“非典型性别特征”就在此前,斯坦福大学两名研究人员开发了一个神经网络,可以通过研究一个面部图像来检测一个人的性取向。研究人员训练了超过35,000张面部图像的神经网络,在同性恋和异性恋之间平均分配。该算法的追踪涉及了遗传或激素相关的特征。这项研究的关键在于影响性取向的产前激素理论(PHT)。这个理论具体讲的是,在子宫中,对性分化负责的主要是雄性激素,也是以后生活中同性恋取向的主要驱动力。研究还指出,这些特定的雄性激素会在一定程度上影响面部的关键特征,这意味着某些面部特征可能与性取向有关。研究发现,男女同性恋倾向于具有“非典型性别特征”,也就是说男同性恋通常趋向于女性化,而女同性恋反之亦然。此外,研究人员发现男同性恋通常拥有比直男更窄的下巴、更长的鼻子以及更大的前额,而女同性恋会拥有更大的下巴和更小的前额。▲图片来源:Te

  • Android中多线程切换的几种方法

    作者:蓝灰_q https://www.jianshu.com/p/31d0852c0760我们知道,多线程是Android开发中必现的场景,很多原生API和开源项目都有多线程的内容,这里简单总结和探讨一下常见的多线程切换方式。 我们先回顾一下Java多线程的几个基础内容,然后再分析总结一些经典代码中对于线程切换的实现方式。几点基础多线程切换,大概可以切分为这样几个内容:如何开启多个线程,如何定义每个线程的任务,如何在线程之间互相通信。Thread Thread可以解决开启多个线程的问题。 Thread是Java中实现多线程的线程类,每个Thread对象都可以启动一个新的线程,注意是可以启动,也可以不启动新线程:thread.run();//不启动新线程,在当前线程执行 thread.start();//启动新线程。复制另外就是Thread存在线程优先级问题,如果为Thread设置较高的线程优先级,就有机会获得更多的CPU资源,注意这里也是有机会,优先级高的Thread不是必然会先于其他Thread执行,只是系统会倾向于给它分配更多的CPU。 默认情况下,新建的Thread和当前Thr

  • 分布式系统常见事务处理机制

    为保障系统的可用性、可靠性以及性能,在分布式系统中,往往会设置数据冗余,即对数据进行复制。举例来说,当一个数据库的副本被破环以后,那么系统只需要转换到其他数据副本就能继续运行下去。另外一个例子,当访问单一服务器管理的数据的进程数不断增加时,系统就需要对服务器的数量进行扩充,此时,对服务器进行复制,随后让它们分担工作负荷,就可以提高性能。但同时,如何保障多个数据节点之间数据的一致以及如何处理分布式事务,将成为为一个复杂的话题。本文将介绍常用的事务处理机制。CAP定理CAP定理(也称为Brewer定理),是由计算机科学家EricBrewer提出的,即在分布式计算机系统不可能同时提供以下全部三个保证:一致性(Consistency):所有节点同一时间看到是相同的数据;可用性(Availability):不管是否成功,确保每一个请求都能接收到响应;分区容错性(Partitiontolerance):系统任意分区后,在网络故障时,仍能操作显然,为了保障性能和可靠性,我们将数据复制多份,分布到多个节点上,同时也带来了一个难点,那就是如何保持各个副本数据的一致性。换句话说,我们选择了AP,则必须要牺

  • docker pull很慢解决办法以及elasticsearch的安装

    dockerpull很慢解决办法##使用阿里云镜像加速器 ##使用阿里云镜像加速器 [root@localhost~]#mkdir-p/etc/docker [root@localhost~]#tee/etc/docker/daemon.json<<-'EOF' { "registry-mirrors":["http://f2d6cb40.m.daocloud.io"] } EOF [root@localhost~]#systemctldaemon-reload [root@localhost~]#systemctlrestartdocker复制安装elasticsearch下载镜像dockerpullelasticsearch:5.6.8复制创建容器dockerrun-di--name=myes-eES_JAVA_OPTS="-Xms256m-Xmx256m"-p9200:9200-p9300:9300elasticsearch:5.6.8复制浏览器输入地址: http://192.168.2

  • scrapy之请求传参、图片爬取与中间件

    请求传参   使用场景:如果解析的数据不在同一个页面中(深度爬取)。   举个例子:假如我们首先爬取了首页数据,然后再解析详情页数据,如何操作? 1#解析首页的岗位名称 2defparse(self,response): 3li_list=response.xpath('//*[@id="main"]/div/div[3]/ul/li') 4forliinli_list: 5#实例化item对象 6item=BossproItem() 7 8detail_page_url='https://www.zhipin.com'+li.xpath('./div/div[1]/div[1]/div/@href').extract_first() 9job_name=li.xpath('.//span[@class="job-name"]//text()').extract() 10 11#分页操作 12ifself.page_num<=5: 13new_url=format(self.url%self.page_num) 14self.page_num+=1 15#对详情页发请求获取

  • [IOS]Xcode各版本官方下载及百度云盘下载, Mac和IOS及Xcode版本历史

    官方下载, 用开发者账户登录,建议用Safari浏览器下载.   官方下载地址:   https://developer.apple.com/xcode/downloads/   百度云盘下载地址 http://yun.baidu.com/share/home?uk=1902433471#category/type=0   Xcode 7 beta3: https://developer.apple.com/services-account/download?path=/Developer_Tools/Xcode_7_beta_3/Xcode_7_beta_3.dmg   Xcode 6  6.4: http://developer.apple.com/devcenter/download.action?path=/Developer_Tools/Xcode_6.4/Xcode_6.4.dmg   6.3.2: http://developer.apple.

  • 求解欧拉函数

    voidgetphi() { phi[1]=1; //起始赋一(1内与1互质的数) for(inti=2;i<=n;i++) { if(!mark[i]){phi[i]=i-1;pri[++tot]=i;} //求质数 for(intj=1;j<=tot;j++) { intx=pri[j]; if(i*x>n)break; mark[i*x]=1; if(i%x==0){phi[i*x]=phi[i]*x;break;} //p(的k次方型欧拉函数变换) //欧拉性质 elsephi[i*x]=phi[i]*phi[x];(i,x互质) } } }复制   注意,在求大数的欧拉函数时,超过根号n的质数要特判(p-1)  

  • MVC 局部加载页面的实例

    我们在做MVC进行某一块的局部刷新,有的使用AJAX请求,有的使用局部页; 下面我给大家推荐一种使用局部页面实现的这种方式: 第一步: 嵌套视图页 <divid="showAudit"> @*操作信息*@ @{Html.RenderPartial("ShowAudit",Model);} </div>复制 ViewCode   第二步: 编写后台代码 [HttpGet] publicActionResultShowAudit(intid) { using(var_ctx=newBtDbContext()) { varmemberSupply=_ctx.MemberSupplys.FirstOrDefault(c=>c.MemberId==id); MemberSupplyModelmodel=newMemberSupplyModel(); if(memberSupply!=null) { model.MemberId=memberSupply.MemberId; model.OrderNumber=memberSupply.Ord

  • linux 字符串 md5sum

    [root@web-master~]#echo-n"helloworld"|md5sum 5eb63bbbe01eeed093cb22bb8f5acdc3- [root@web-master~]#echo-n"helloworld"|md5sum|cut-d""-f1 5eb63bbbe01eeed093cb22bb8f5acdc3 复制 命令解释: md5sum:显示或检查MD5(128-bit)校验和,若没有文件选项,或者文件处为"-",则从标准输入读取。 echo-n:不打印换行符。(注意:echo-n后面的-n参数必须加上,这样算出的字符串的md5值才正确) cut:cut用来从标准输入或文本文件中剪切列或域。剪切文本可以将之粘贴到一个文本文件。-d指定与空格和tab键不同的域分隔符。-f1表示第一个域。 作者:DaleyZou 出处:http://www.cnblogs.com/daleyzou/

  • Python18天训练营第二课&lt;基础1&gt;

    目录第一个python程序环境四则运算备注变量命名规则用于接收命令行的语句数据类型整型浮点数字符串布尔类型类型转换流程控制分支语句for循环while循环练习: 第一个python程序 环境 python-3.6.8 print("helloworld!") 复制 四则运算 +加-减*乘/除//整除%取余**幂 复制 备注 1.print()是python的函数指令,用于让计算机打印括号中的内容到标准输出 2.exit()是python交互环境下的退出函数指令 复制 变量 命名规则 只能由英文字母大小写,数字,下划线组成 不能以数字开头 避免和python的关键字和保留字冲突 用于接收命令行的语句 input()函数 EXAMPLES: num=input("输入你的数字:") print("num:",num) 复制 数据类型 Type()函数用于对数据类型进行判断 整型 int 复制 浮点数 float 复制 字符串 str 复制 布尔类型 bool 只有True和False,首字母必须大写 除了0代表False以外,其余都是True 布尔运算: 与

  • socket(int family, int type, int protocol)各参数define

    family: /*Supportedaddressfamilies.*/ #defineAF_UNSPEC0 #defineAF_UNIX1/*Unixdomainsockets */ #defineAF_LOCAL1/*POSIXnameforAF_UNIX */ #defineAF_INET2/*InternetIPProtocol */ #defineAF_AX253/*AmateurRadioAX.25 */ #defineAF_IPX4/*NovellIPX */ #defineAF_APPLETALK5/*AppleTalkDDP */ #defineAF_NETROM6/*AmateurRadioNET/ROM */ #defineAF_BRIDGE7/*Multiprotocolbridge */ #defineAF_ATMPVC8/*ATMPVCs */ #defineAF_X259/*ReservedforX.25project */ #defineAF_INET610/*IPversion6 */ #defineAF_ROSE11/*AmateurRadioX.

  • 部署Maven项目到tomcat报错:java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListener【转】

    Maven项目下update maven后Eclipse报错:java.lang.ClassNotFoundException: ContextLoaderL   严重:Errorconfiguringapplicationlistenerofclassorg.springframework.web.context.ContextLoaderListener java.lang.ClassNotFoundException:org.springframework.web.context.ContextLoaderListener atorg.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1678) atorg.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1523) atorg.apache.catalina.core.DefaultInstanceMan

  • mysql5.7安装和卸载过程

    安装mysql5.7 点击下面链接下载 mysql-5.7.27-winx64.zip压缩文件 链接:https://pan.baidu.com/s/1CF5mmKkZkD_hxsjFOQJrzw 提取码:dm0r 1.将压缩包 解压 把mysql-5.7.27-winx64.zip解压放到D盘目录下   解压后的文件夹中没有data目录   2.添加环境变量 将mysql文件夹中bin目录添加到PATH环境变量中  D:\mysql-5.7.27-winx64\bin 右击 计算机--属性    找到  高级系统设置--环境变量    3.创建配置文件  在mysql安装目录下创建my.cnf配置文件 文件中具体内容如下: [mysqld] basedir=D:/mysql-5.7.27-winx64 datadir=D:/mysql-5.7.27-winx64/data character-set-server=utf8

  • BootstrapValidator 表单验证超详细教程

    https://www.cnblogs.com/keepruning/p/9153545.html talkischeap.showmethecode.

  • CUDA初试

    1.基本概念   CUDA,全称是ComputeUnifiedDeviceArchitecture,意即统一计算架构,是NVIDIA推出的一种整合技术,开发者可以利用NVIDIA的GeForce8以后的GPU和较新的QuadroGPU进行计算。——维基百科   利用CUDA这个平台,可以方便地使用GPU来加速程序的数据运算。GPU对于深度学习这类领域非常重要,因为其具有强大的并行计算能力和浮点运算能力。   CUDA的编程模型将CPU作为主机(Host),将GPU作为设备(Device),CPU用来控制整体调度和程序逻辑,GPU负责执行高度线程化的数据并行部分。   运行在GPU上的程序被称为内核。 2.程序的一般步骤   01.分配主机储存器并初始化   02.分配设备储存器   03.将已经初始化的主机储存器内容复制到已分配的设备储存器上   04.GPU进行计算   05.将计算完的结果从设备复制到主机上   06.处理该结果数据 3.CUDA的线程层次   主要是三个层次,网格(Grid)、线程块(Block)、线程(Thread) (抱歉,下图的每个第一列的(1,0)应该改为

  • Python:开发_基本流程

    Python开发——基本流程 开发:   开发运行在操作系统之上的软件   操作系统是运行在硬件上的另一种“软件”   -编码  ASCII码      是最早美国用的标准信息交换码,把所有的字母的大小写,各种符号用二进制来表示,共有256中,加入些拉丁文等字符,1bytes代表一个字符;       Unicode(万国码) 是为了统一世界各国语言的不用,统一用2个bytes代表一个字符,可以表达2**16=65556个,称为万国语言,特点:速度快,但浪费空间;       utf-8        为了改变Unicode的这种缺点,规定1个英文字符用1个字节表示,1个中文字符用3个字节表示,特点:节省空间,速度慢;       GBK        中文的字符编码,用2个字节代表一个字符   -开发语言:        高级语言:Python、Java、PHP、c#、Go、ruby、C++....,开发效率高,执行效率低  ===字节码        低级语言:汇编、C、机器语言(二进制)....,开发效率低,执行效率高         ===机器码   -机器

相关推荐

推荐阅读