HashMap、LinkedHashMap、ConcurrentHashMap底层实现

本文详细介绍了Java中的三种Map实现:HashMap、LinkedHashMap和ConcurrentHashMap。HashMap基于数组和链表(或红黑树)实现,线程不安全;LinkedHashMap则在HashMap基础上增加了双向链表,保证了插入或访问顺序;ConcurrentHashMap是线程安全的Map,其内部机制保证了并发环境下的高效操作。

HashMap: (JDK1.8)

     HashMap是基于map接口实现的,允许使用null值,null键,由于key不允许重复,因此只有一个key为null,另外HashMap不保存元素放入顺序,是线程不安全的。

  HashMap底层是基于数组实现, 数组中每一项是单向链表,即数据和链表结合体,当链表长度大于一定阀值,转为红黑树

HashMap在底层将key-value当成一个整体,就是一个node对象,采用一个node[]数组保存

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and load factor.
 * 构建一个空HashMap 具有指定的初始容量和负荷因子
 * @param  initialCapacity the initial capacity 初始容量
 * @param  loadFactor      the load factor 负荷因子
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive (异常)
 * MAXIMUM_CAPACITY = 1<<30 == 1073741824
 */

 

继承关系

 

基础属性:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 // 初始容量 16

static final int MAXIMUM_CAPACITY = 1 << 30; //最大容量

static final float DEFAULT_LOAD_FACTOR = 0.75f; //默认负荷系数

static final int TREEIFY_THRESHOLD = 8; 

static final int UNTREEIFY_THRESHOLD = 6;

static final int MIN_TREEIFY_CAPACITY = 64; 
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;//用于定位数组索引的位置
    final K key;
    V value;
    Node<K,V> next; // 下一个node

    Node(int hash, K key, V value, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }

    public final K getKey()        { return key; }
    public final V getValue()      { return value; }
    public final String toString() { return key + "=" + value; }

    public final int hashCode() {
        return Objects.hashCode(key) ^ Objects.hashCode(value);
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (o == this)
            return true;
        if (o instanceof Map.Entry) {
            Map.Entry<?,?> e = (Map.Entry<?,?>)o;
            if (Objects.equals(key, e.getKey()) &&
                Objects.equals(value, e.getValue()))
                return true;
        }
        return false;
    }
}

 

 

计算 hashMap hash

 

hashMap put方法

put函数大致的思路为:

  1. 对key的hashCode()做hash,然后再计算index;
  2. 如果没碰撞直接放到bucket里;
  3. 如果碰撞了,以链表的形式存在buckets后;
  4. 如果碰撞导致链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树;
  5. 如果节点已经存在就替换old value(保证key的唯一性)
  6. 如果bucket满了(超过load factor*current capacity),就要resize。
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

//生成hash
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;//创建一个新的table数组,并且获取该数组的长度

//根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加

       if ((p = tab[i = (n - 1) & hash]) == null)
          tab[i] = newNode(hash, key, value, null);
    else { // 如果对应节点存在
        Node<K,V> e; K k;
//判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode) //判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {// 该链为链表
//遍历table[i],判断链表长度是否大于TREEIFY_THRESHOLD(默认值为8),
//大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                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;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

 

1.7中 HashMap put方法

public V put(K key, V value) {  
    // 如果table引用指向成员变量EMPTY_TABLE,那么初始化HashMap(设置容量、临界值,新的Entry数组引用)
    if (table == EMPTY_TABLE) {
        inflateTable(threshold);
    }
    // 若“key为null”,则将该键值对添加到table[0]处,遍历该链表,如果有key为null,则将value替换。没有就创建新Entry对象放在链表表头
    // 所以table[0]的位置上,永远最多存储1个Entry对象,形成不了链表。key为null的Entry存在这里 
    if (key == null)  
        return putForNullKey(value);  
    // 若“key不为null”,则计算该key的哈希值
    int hash = hash(key);  
    // 搜索指定hash值在对应table中的索引
    int i = indexFor(hash, table.length);  
    // 循环遍历table数组上的Entry对象,判断该位置上key是否已存在
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
        Object k;  
        // 哈希值相同并且对象相同
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
            // 如果这个key对应的键值对已经存在,就用新的value代替老的value,然后退出!
            V oldValue = e.value;  
            e.value = value;  
            e.recordAccess(this);  
            return oldValue;  
        }  
    }  
    // 修改次数+1
    modCount++;
    // table数组中没有key对应的键值对,就将key-value添加到table[i]处 
    addEntry(hash, key, value, i);  
    return null;  
}  

 

hashMap 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) {
        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);
        }
    }
    return null;
}

扩容机制

void resize(int newCapacity) {   //传入新的容量
     Entry[] oldTable = table;    //引用扩容前的Entry数组
     int oldCapacity = oldTable.length;         
     if (oldCapacity == MAXIMUM_CAPACITY) {  //扩容前的数组大小如果已经达到最大(2^30)了
          threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
         return;
     }
 
     Entry[] newTable = new Entry[newCapacity];  //初始化一个新的Entry数组
     transfer(newTable);                         //!!将数据转移到新的Entry数组里
     table = newTable;                           //HashMap的table属性引用新的Entry数组
   threshold = (int)(newCapacity * loadFactor);//修改阈值
}
 

LinkedHashMap(JDK1.8)

继承与HashMap,底层使用hash表和双向链表来保存所有元素,允许使用null值和null键

 

/ /双向链表的头部(eldest) 

transient LinkedHashMap.Entry<K,V> head;

//双向链表的尾部(youngest) 

transient LinkedHashMap.Entry<K,V> tail;

//accessOrder为true时,按访问顺序排序,false时,按插入顺序排序 

final boolean accessOrder

 

LinkedHashMap的Entry是继承与Node类,也就是LinkedHashMap与HashMap的根本区别所在,Node是链表形式,只有next与下一个元素进行连接,而Entry的链表有before和after两个连接点。

 

区别是将节点变成Entry,并且按照链表方式将元素有序连接

 

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
// 若访问顺序为true,且访问的对象不是尾结点
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a != null)
            a.before = b;
        else
            last = b;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        tail = p;
        ++modCount;
    }
}
就是说在进行put之后就算是对节点的访问了,那么这个时候就会更新链表,把最近访问的放到最后,保证链表。关键参数是accessOrder,
这个参数只有在public LinkedHashMap(int initialCapacity, float loadFactor,boolean accessOrder) 中可以手动设置为true,其余时候都默认为false

 

LinkedHashMap get方法

public V get(Object key) {
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    if (accessOrder)//安装访问顺序而不是插入的顺序,此时会发生结构改变
        afterNodeAccess(e);
    return e.value;
}

ConcurrentHashMap

    多线程安全的,底层数据与HashMap数据结构相同。

  ConcurrentHashMap在底层将key-value当成一个整体处理,也就是一个entry

// 表的最大容量
    private static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认表的大小
    private static final int DEFAULT_CAPACITY = 16;
    // 最大数组大小
    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    // 默认并发数
    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    // 装载因子
    private static final float LOAD_FACTOR = 0.75f;
    // 转化为红黑树的阈值
    static final int TREEIFY_THRESHOLD = 8;
    // 由红黑树转化为链表的阈值
    static final int UNTREEIFY_THRESHOLD = 6;
    // 转化为红黑树的表的最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 每次进行转移的最小值
    private static final int MIN_TRANSFER_STRIDE = 16;
    // 生成sizeCtl所使用的bit位数
    private static int RESIZE_STAMP_BITS = 16;
    // 进行扩容所允许的最大线程数
    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
    // 记录sizeCtl中的大小所需要进行的偏移位数
    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;    
    // 一系列的标识
    static final int MOVED     = -1; // hash for forwarding nodes
    static final int TREEBIN   = -2; // hash for roots of trees
    static final int RESERVED  = -3; // hash for transient reservations
    static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash

 

ConcurrentHashMap put方法

public V put(K key, V value) {
    return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException(); // 键或值为空,抛出异常
        // 键的hash值经过计算获得hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) { // 无限循环
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0) // 表为空或者表的长度为0
                // 初始化表
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 表不为空并且表的长度大于0,并且该桶不为空
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null))) // 比较并且交换值,如tab的第i项为空则用新生成的node替换
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED) // 该结点的hash值为MOVED
                // 进行结点的转移(在扩容的过程中)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) { // 加锁同步
                    if (tabAt(tab, i) == f) { // 找到table表下标为i的节点
                        if (fh >= 0) { // 该table表中该结点的hash值大于0
                            // binCount赋值为1
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) { // 无限循环
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) { // 结点的hash值相等并且key也相等
                                    // 保存该结点的val值
                                    oldVal = e.val;
                                    if (!onlyIfAbsent) // 进行判断
                                        // 将指定的value保存至结点,即进行了结点值的更新
                                        e.val = value;
                                    break;
                                }
                                // 保存当前结点
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) { // 当前结点的下一个结点为空,即为最后一个结点
                                    // 新生一个结点并且赋值给next域
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    // 退出循环
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) { // 结点为红黑树结点类型
                            Node<K,V> p;
                            // binCount赋值为2
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) { // 将hash、key、value放入红黑树
                                // 保存结点的val
                                oldVal = p.val;
                                if (!onlyIfAbsent) // 判断
                                    // 赋值结点value值
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) { // binCount不为0
                    if (binCount >= TREEIFY_THRESHOLD) // 如果binCount大于等于转化为红黑树的阈值
                        // 进行转化
                        treeifyBin(tab, i);
                    if (oldVal != null) // 旧值不为空
                        // 返回旧值
                        return oldVal;
                    break;
                }
            }
        }
        // 增加binCount的数量
        addCount(1L, binCount);
        return null;
    }

 

<think> 首先,用户的问题是如何将HashMap转换为ConcurrentHashMap。我需要基于提供的引用和系统指令来回答。 回顾系统指令: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 用户的问题是关于Java编程的,涉及到HashMapConcurrentHashMap的转换。引用提供了相关信息: - [^1]:解释了ConcurrentHashMap的优势,如锁粒度更小。 - [^2]:讨论了HashMapConcurrentHashMap的关系。 - [^3]:详细描述了HashMapConcurrentHashMap底层实现,包括在JDK 1.7和1.8中的差异。 - [^4]:提到了LinkedHashMap,但用户的问题是关于HashMapConcurrentHashMap的转换。 用户的需求是:“我想查找将HashMap转换为ConcurrentHashMap的方法”,所以我需要提供一个清晰的、逐步的方法来实现这个转换。 在Java中,将HashMap转换为ConcurrentHashMap并不直接有内置方法,因为它们是不同的类。通常,我们需要创建一个新的ConcurrentHashMap实例,并将HashMap的内容复制过去。具体步骤: 1. 创建一个新的ConcurrentHashMap实例。 2. 使用putAll方法或遍历HashMap的entrySet来添加所有键值对到ConcurrentHashMap中。 例如: - ConcurrentHashMap<Key, Value> concurrentMap = new ConcurrentHashMap<>(existingHashMap); - 或者使用putAll:concurrentMap.putAll(existingHashMap); 从引用[^3]中,我了解到ConcurrentHashMap在JDK 1.8中使用了CAS操作等来减少锁竞争,这确保了线程安全。 现在,构建回答结构: - 回答应该结构清晰,逐步解决问题。 - 用中文回答。 - 在引用末尾添加引用标识。 - 生成相关问题。 回答结构: 1. **介绍转换方法**:简要解释为什么需要转换(如果需要的话),然后给出具体步骤。 2. **代码示例**:提供一个简单的Java代码片段来演示转换。 3. **注意事项**:提醒线程安全和性能问题。 4. **相关问题**:生成几个相关问题。 生成相关问题: - 基于主题:HashMap vs ConcurrentHashMap、并发编程等。 - 例如: 1. 如何在多线程环境中安全使用ConcurrentHashMap? 2. HashMapConcurrentHashMap的主要区别是什么? 3. ConcurrentHashMap在JDK 1.8中的优化有哪些? 确保LaTeX语法:由于这个问题不涉及数学表达式,可能不需要使用$...$或$$...$$,但如果提到任何公式(如性能讨论),就使用。否则,跳过。 添加引用: - 例如,在讨论ConcurrentHashMap线程安全时引用[^1]或[^3]。 完整回答: - 开头:直接回答用户问题。 - 主体:步骤和代码。 - 结尾:注意事项和相关问题。 </think> 以下是基于Java编程语言,如何将`HashMap`转换为`ConcurrentHashMap`的详细解答。我会逐步解释方法、提供代码示例,并讨论相关注意事项。`ConcurrentHashMap`是Java中线程安全的哈希表实现,它在多线程环境下提供更好的并发性能,因为它使用更细粒度的锁机制(如CAS操作),从而减少锁竞争[^1][^3]。 ### 转换方法概述 在Java中,`HashMap`和`ConcurrentHashMap`都属于`java.util.Map`接口,但它们没有直接的兼容转换方法(例如,没有内置的`convertToConcurrentHashMap()`函数)。转换的核心是创建一个新的`ConcurrentHashMap`实例,并将`HashMap`的所有键值对复制到新实例中。这可以通过两种常用方式实现: 1. **使用`ConcurrentHashMap`构造函数**:直接传入`HashMap`实例进行初始化。 2. **使用`putAll()`方法**:创建一个空的`ConcurrentHashMap`,然后批量添加`HashMap`的内容。 这两种方法都是线程安全的,因为`ConcurrentHashMap`在内部处理并发访问,但前提是转换过程本身在单线程环境中执行(避免在复制过程中并发修改`HashMap`)。 ### 逐步转换步骤 1. **创建或获取原始`HashMap`**:假设你有一个已存在的`HashMap<String, Integer>`,你想将其转换为线程安全的`ConcurrentHashMap<String, Integer>`。 2. **初始化`ConcurrentHashMap`实例**: - **方式1:通过构造函数直接转换** 这是最简单高效的方法,适用于JDK 1.8及以上版本。它会调用`ConcurrentHashMap`的内部复制逻辑。 ```java import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; public class MapConverter { public static void main(String[] args) { // 原始HashMap示例 HashMap<String, Integer> hashMap = new HashMap<>(); hashMap.put("Apple", 1); hashMap.put("Banana", 2); // 转换为ConcurrentHashMap ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>(hashMap); // 验证输出 System.out.println(concurrentMap); // 输出: {Apple=1, Banana=2} } } ``` - **方式2:使用`putAll()`方法** 适用于需要额外自定义初始化(如指定初始容量或负载因子)的场景。先创建空`ConcurrentHashMap`,再复制数据。 ```java import java.util.HashMap; import java.util.concurrent.ConcurrentHashMap; public class MapConverter { public static void main(String[] args) { HashMap<String, Integer> hashMap = new HashMap<>(); hashMap.put("Apple", 1); hashMap.put("Banana", 2); // 创建空ConcurrentHashMap ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>(); // 复制所有键值对 concurrentMap.putAll(hashMap); // 验证输出 System.out.println(concurrentMap); // 输出: {Apple=1, Banana=2} } } ``` ### 注意事项 - **线程安全**:转换后,`ConcurrentHashMap`是线程安全的,可以放心在多线程环境中使用(例如,多个线程同时读写数据)。但转换过程本身(如调用构造函数或`putAll()`)建议在单线程中进行,以避免源`HashMap`在被复制时被并发修改导致数据不一致[^1][^3]。 - **性能影响**:复制操作的时间复杂度为$O(n)$(其中$n$是`HashMap`的大小)。对于大型Map(如数百万条条目),这可能导致短暂延迟。在JDK 1.8中,`ConcurrentHashMap`使用红黑树优化查找性能(当链表长度超过阈值时),因此转换后查询效率较高[^3]。 - **版本兼容性**:以上代码在JDK 1.7+中有效。JDK 1.8的`ConcurrentHashMap`实现更高效(使用CAS和sychronized块而非分段锁),推荐使用最新JDK版本[^3]。 - **Key和Value类型**:确保泛型类型匹配(如`String`和`Integer`),否则编译错误。 - **为什么不直接使用`ConcurrentHashMap`?** 如果你的应用从开始就需要线程安全,可以直接初始化`ConcurrentHashMap`,避免转换开销。HashMap适用于单线程场景,而ConcurrentHashMap专为高并发设计[^2][^4]。 ### 相关问题 1. 如何在多线程环境中安全地使用ConcurrentHashMap进行操作(如put和get)? 2. HashMapConcurrentHashMap在性能和线程安全方面有哪些主要区别? 3. ConcurrentHashMap在JDK 1.8中的底层优化机制是什么(如CAS和红黑树)? 4. LinkedHashMapConcurrentHashMap在有序性和并发性上有什么差异?[^4]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值