ConcurrentHashMap核心原理,彻底整明白了!

目录

一、ConcurrentHashmap简介

二、键属性及类

三、重点方法讲解

四、总结


一、ConcurrentHashmap简介

在使用HashMap时在多线程情况下扩容会出现CPU接近100%的情况,因为hashmap并不是线程安全的,通常我们可以使用在java体系中古老的hashtable类,该类基本上所有的方法都采用synchronized进行线程安全的控制,可想而知,在高并发的情况下,每次只有一个线程能够获取对象监视器锁,这样的并发性能的确不令人满意。另外一种方式通过Collections的Map<K,V> synchronizedMap(Map<K,V> m)将hashmap包装成一个线程安全的map。比如SynchronzedMap的put方法源码为:

public V put(K key, V value) {
    synchronized (mutex) {return m.put(key, value);}
}

实际上SynchronizedMap实现依然是采用synchronized独占式锁进行线程安全的并发控制的。同样,这种方案的性能也是令人不太满意的。针对这种境况,Doug Lea大师不遗余力的为我们创造了一些线程安全的并发容器,让每一个java开发人员倍感幸福。相对于hashmap来说,ConcurrentHashMap就是线程安全的map,其中利用了锁分段的思想提高了并发度

ConcurrentHashMap在JDK1.6的版本网上资料很多,有兴趣的可以去看看。 JDK 1.6版本关键要素:

  1. segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障;

  2. segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表。

而到了JDK 1.8的ConcurrentHashMap就有了很大的变化,光是代码量就足足增加了很多。1.8版本舍弃了segment,并且大量使用了synchronized,以及CAS无锁操作以保证ConcurrentHashMap操作的线程安全性。至于为什么不用ReentrantLock而是Synchronzied呢?实际上,synchronzied做了很多的优化,包括偏向锁,轻量级锁,重量级锁,可以依次向上升级锁状态,但不能降级,因此,使用synchronized相较于ReentrantLock的性能会持平甚至在某些情况更优,具体的性能测试可以去网上查阅一些资料。另外,底层数据结构改变为采用数组+链表+红黑树的数据形式。

二、键属性及类

在了解ConcurrentHashMap的具体方法实现前,我们需要系统的来看一下几个关键的地方。

ConcurrentHashMap的关键属性

  1. table volatile Node<K,V>[] table://装载Node的数组,作为ConcurrentHashMap的数据容器,采用懒加载的方式,直到第一次插入数据的时候才会进行初始化操作,数组的大小总是为2的幂次方。

  2. nextTable volatile Node<K,V>[] nextTable; //扩容时使用,平时为null,只有在扩容的时候才为非null

  3. sizeCtl volatile int sizeCtl; 该属性用来控制table数组的大小,根据是否初始化和是否正在扩容有几种情况: 当值为负数时:如果为-1表示正在初始化,如果为-N则表示当前正有N-1个线程进行扩容操作; 当值为正数时:如果当前数组为null的话表示table在初始化过程中,sizeCtl表示为需要新建数组的长度; 若已经初始化了,表示当前数据容器(table数组)可用容量也可以理解成临界值(插入节点数超过了该临界值就需要扩容),具体指为数组的长度n 乘以 加载因子loadFactor; 当值为0时,即数组长度为默认初始值。

  4. sun.misc.Unsafe U 在ConcurrentHashMapde的实现中可以看到大量的U.compareAndSwapXXXX的方法去修改ConcurrentHashMap的一些属性。这些方法实际上是利用了CAS算法保证了线程安全性,这是一种乐观策略,假设每一次操作都不会产生冲突,当且仅当冲突发生的时候再去尝试。而CAS操作依赖于现代处理器指令集,通过底层CMPXCHG指令实现。CAS(V,O,N)核心思想为:若当前变量实际值V与期望的旧值O相同,则表明该变量没被其他线程进行修改,因此可以安全的将新值N赋值给变量;若当前变量实际值V与期望的旧值O不相同,则表明该变量已经被其他线程做了处理,此时将新值N赋给变量操作就是不安全的,在进行重试。而在大量的同步组件和并发容器的实现中使用CAS是通过sun.misc.Unsafe类实现的,该类提供了一些可以直接操控内存和线程的底层操作,可以理解为java中的“指针”。该成员变量的获取是在静态代码块中:

    static {
        try {
            U = sun.misc.Unsafe.getUnsafe();
            .......
        } catch (Exception e) {
            throw new Error(e);
        }
    }

ConcurrentHashMap中关键内部类

  1. Node Node类实现了Map.Entry接口,主要存放key-value对,并且具有next域

    static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            volatile V val;
            volatile Node<K,V> next;
            ......
    }

另外可以看出很多属性都是用volatile进行修饰的,也就是为了保证内存可见性。

  1. TreeNode 树节点,继承于承载数据的Node类。而红黑树的操作是针对TreeBin类的,从该类的注释也可以看出,也就是TreeBin会将TreeNode进行再一次封装

    **
     * Nodes for use in TreeBins
     */
    static final class TreeNode<K,V> extends Node<K,V> {
            TreeNode<K,V> parent;  // red-black tree links
            TreeNode<K,V> left;
            TreeNode<K,V> right;
            TreeNode<K,V> prev;    // needed to unlink next upon deletion
            boolean red;
            ......
    }

  2. TreeBin 这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象。

    static final class TreeBin<K,V> extends Node<K,V> {
            TreeNode<K,V> root;
            volatile TreeNode<K,V> first;
            volatile Thread waiter;
            volatile int lockState;
            // values for lockState
            static final int WRITER = 1; // set while holding write lock
            static final int WAITER = 2; // set when waiting for write lock
            static final int READER = 4; // increment value for setting read lock
            ......
    }

  3. ForwardingNode 在扩容时才会出现的特殊节点,其key,value,hash全部为null。并拥有nextTable指针引用新的table数组。

    static final class ForwardingNode<K,V> extends Node<K,V> {
        final Node<K,V>[] nextTable;
        ForwardingNode(Node<K,V>[] tab) {
            super(MOVED, null, null, null);
            this.nextTable = tab;
        }
       .....
    }

CAS关键操作

在上面我们提及到在ConcurrentHashMap中会大量使用CAS修改它的属性和一些操作。因此,在理解ConcurrentHashMap的方法前我们需要了解下面几个常用的利用CAS算法来保障线程安全的操作。

  1. tabAt

    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE); } 该方法用来获取table数组中索引为i的Node元素。

  2. casTabAt

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) { return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v); }

    利用CAS操作设置table数组中索引为i的元素

  3. setTabAt

    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) { U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v); }

    该方法用来设置table数组中索引为i的元素

三、重点方法讲解

在熟悉上面的这核心信息之后,我们接下来就来依次看看几个常用的方法是怎样实现的。

3.1 实例构造器方法

在使用ConcurrentHashMap第一件事自然而然就是new 出来一个ConcurrentHashMap对象,一共提供了如下几个构造器方法:

// 1. 构造一个空的map,即table数组还未初始化,初始化放在第一次插入数据时,默认大小为16
ConcurrentHashMap()
// 2. 给定map的大小
ConcurrentHashMap(int initialCapacity) 
// 3. 给定一个map
ConcurrentHashMap(Map<? extends K, ? extends V> m)
// 4. 给定map的大小以及加载因
### 回答1: ConcurrentHashMap 是 Java 并发编程中的一个常用的类,它是线程安全的 HashMap。ConcurrentHashMap 的实现原理是利用了分段锁(Segment Locking)的思想。 在 ConcurrentHashMap 中,数据被分为多个段,每一段都有一个独立的锁,这样多个线程可以同时修改不同段的数据,从而实现并发读写。 当一个线程要对 ConcurrentHashMap 中的某一段数据进行修改时,它需要先获得该段数据对应的锁,在修改完后再释放锁。这样,其他线程就无法修改这一段数据,从而保证了数据的完性。 同时,ConcurrentHashMap 还使用了链表和树来存储数据,提高了查询效率。总的来说,ConcurrentHashMap 是一个高效、线程安全的数据结构。 ### 回答2: ConcurrentHashMap是Java中的一个并发安全的哈希表实现,它是线程安全的,并且提供了比Hashtable更好的并发性能。 ConcurrentHashMap的实现原理主要基于以下几个关键技术: 1. 分段锁:ConcurrentHashMap内部分为多个段(Segment),每个段维护了一部分键值对。不同的段可以由不同的线程同时访问,从而实现了多线程之间的并发访问。每个段都相当于一个独立的小HashTable,维护了一部分数据,包含了一个Entry数组,每个Entry继承自一个链表的结构,在发生哈希冲突时,新元素会被插入到链表的头部。 2. CAS操作:ConcurrentHashMap使用了CAS(Compare and Set)操作实现对元素的插入、更新和删除。CAS是一种无锁并发操作,通过比较内存中的值与预期值进行原子性的修改,可以避免了使用传统的加锁机制带来的性能开销。 3. 锁分离:ConcurrentHashMap的设计中,读操作不需要加锁,多个线程可以同时进行读操作。而写操作会加锁,但由于使用了分段锁的机制,不同的线程可以同时对不同的段进行写操作,提高了并发性能。 通过上述的原理ConcurrentHashMap实现了读操作的高并发性能,同时确保了写操作的线程安全性。它适用于多线程场景下的高并发读写操作,比如在缓存、并发任务处理等方面的应用。然而,需要注意的是,ConcurrentHashMap虽然在读写操作上提供了高性能,但在迭代时,由于没有对全表进行加锁操作,可能会出现数据不一致的情况,所以在迭代操作时需要使用迭代器的额外方法来确保数据一致性。 ### 回答3: ConcurrentHashMap是Java中线程安全的哈希表的实现,其实现原理如下。 首先,ConcurrentHashMap将数据存储在一个数组中,每个元素称为“桶”,每个桶又是一个链表或红黑树的节点。当多个线程同时访问ConcurrentHashMap时,它使用锁分段技术,将个数组分割成多个段,每个段都有一个独立的锁。 在插入元素时,ConcurrentHashMap首先根据元素的哈希值确定要放入哪个桶中。然后,它会尝试获取该桶的独立锁,如果成功获取锁,则将元素插入到对应的链表或红黑树中。如果无法获取锁,则会尝试升级为全局锁以保证线程安全性。 在读取元素时,ConcurrentHashMap允许同时进行多个读操作,因为读操作不会涉及到对数据的修改。每个段都有一个读锁,多个线程可以同时获取读锁并访问对应段中的链表或红黑树。 在更新元素时,ConcurrentHashMap会对个桶或树进行操作。在这之前,它首先会获取该段的写锁,以确保不会有其他线程同时修改数据。然后,它会进行元素的查找、删除或插入操作,并根据需要将链表转换为红黑树。 总结起来,ConcurrentHashMap通过锁分段技术和读写锁实现了线程安全的哈希表。它允许多个线程同时进行读操作,提高了并发性能。而在进行写操作时,它会使用锁来确保数据的一致性和线程安全性。这使得ConcurrentHashMap成为了并发编程中常用的数据结构
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值