《Java源码分析》:ConcurrentHashMap JDK1.8

《Java源码分析》:ConcurrentHashMap JDK1.8

最近一直在看关于J.U.C中的源码,了解原子操作,了解锁机制,了解多线程并发等等。但是ConcurrentHashMap一直拖着到今天才算告一段落。

也要感谢ConcurrentHashMap这个类,刚开始就是想弄懂里面的工作原理,但是,无奈看了网上关于介绍ConcurrentHashMap这个类的资料或博客都是基于JDK1.8以前的,而刚好此类在JDK1.8之后有很大的变化。因此,由于里面涉及到关于原子操作CAS,自己以前并不知道是什么,于是就开始对原子操作进行了解,看了java.util.concurrent.atom包下相关类源码对其有了一定的了解。接着为了了解锁机制,看了java.util.concurrent.lock包下相关的类库,对锁机制有了大概的了解之后,看了线程池相关的类,对线程池也有了一定的了解。

关于阻塞队列相关的类,自己也大致看了下,但是并没有形成相应的博文,以后有时间重新来了解他们的时候才记录吧。整个过程大概花费了我将近一个来月的时间,虽然对看过的类库的内部实现都只是一个大致的了解,但是确实收获还是挺多的。让我们更好的明白在多线程并发中他们是如何来工作的。

回到正题,刚好借着今天星期天,花了将近一天的时间来看ConcurrentHashMap的实现原理,总算看了一个大概,有了一个大致的了解。也就有了这篇博文。

ConcurrentHashMap 在JDK1.8版本以前的实现原理

既然本篇博文的标题明确的标出了是基于JDK1.8版本的,也就暗示了这个版本和以前的版本关于ConcurrentHashMap有些许的不同,对吧。x

下面我们就先借助网上的资料来看下以前版本的ConcurrentHashMap的实现思路。

我们都知道HashMap是线程不安全的。Hashtable是线程安全的。看过Hashtable源码的我们都知道Hashtable的线程安全是采用在每个方法来添加了synchronized关键字来修饰,即Hashtable是针对整个table的锁定,这样就导致HashTable容器在竞争激烈的并发环境下表现出效率低下。

效率低下的原因说的更详细点:是因为所有访问HashTable的线程都必须竞争同一把锁。当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。

基于Hashtable的缺点,人们就开始思考,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率呢??这就是我们的“锁分离”技术,这也是ConcurrentHashMap实现的基础。

ConcurrentHashMap使用的就是锁分段技术,ConcurrentHashMap由多个Segment组成(Segment下包含很多Node,也就是我们的键值对了),每个Segment都有把锁来实现线程安全,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

因此,关于ConcurrentHashMap就转化为了对Segment的研究。这是因为,ConcurrentHashMap的get、put操作是直接委托给Segment的get、put方法,但是自己上手上的JDK1.8的具体实现确不想网上这些博文所介绍的。因此,就有了本篇博文的介绍。

推荐几个JDK1.8以前版本的关于ConcurrentHashMap的原理分析,方便大家比较。

1、http://www.iteye.com/topic/344876

2、http://ifeve.com/concurrenthashmap/

如需要更多,请自己网上搜索即可。

下面就开始JDK1.8版本中ConcurrentHashMap的介绍。

JDK1.8 版本中ConcurrentHashMap介绍

1、前言

首先要说明的几点:

1、JDK1.8的ConcurrentHashMap中Segment虽保留,但已经简化属性,仅仅是为了兼容旧版本。

2、ConcurrentHashMap的底层与Java1.8的HashMap有相通之处,底层依然由“数组”+链表+红黑树来实现的,底层结构存放的是TreeBin对象,而不是TreeNode对象;

3、ConcurrentHashMap实现中借用了较多的CAS算法,unsafe.compareAndSwapInt(this, valueOffset, expect, update); CAS(Compare And Swap),意思是如果valueOffset位置包含的值与expect值相同,则更新valueOffset位置的值为update,并返回true,否则不更新,返回false。

ConcurrentHashMap既然借助了CAS来实现非阻塞的无锁实现线程安全,那么是不是就没有用锁了呢??答案:还是使用了synchronized关键字进行同步了的,在哪里使用了呢?在操作hash值相同的链表的头结点还是会synchronized上锁,这样才能保证线程安全。

看完ConcurrentHashMap整个类的源码,给自己的感觉就是和HashMap的实现基本一模一样,当有修改操作时借助了synchronized来对table[i]进行锁定保证了线程安全以及使用了CAS来保证原子性操作,其它的基本一致,例如:ConcurrentHashMap的get(int key)方法的实现思路为:根据key的hash值找到其在table所对应的位置i,然后在table[i]位置所存储的链表(或者是树)进行查找是否有键为key的节点,如果有,则返回节点对应的value,否则返回null。思路是不是很熟悉,是不是和HashMap中该方法的思路一样。所以,如果你也在看ConcurrentHashMap的源码,不要害怕,思路还是原来的思路,只是多了些许东西罢了。

2、ConcurrentHashMap类中相关属性的介绍

为了方便介绍此类后面的实现,这里需要先将此类中的一些属性给介绍下。

sizeCtl最重要的属性之一,看源码之前,这个属性表示什么意思,一定要记住。

0、private transient volatile int sizeCtl;//控制标识符

此属性在源码中给出的注释如下:

     /**
        * Table initialization and resizing control.  When negative, the
        * table is being initialized or resized: -1 for initialization,
        * else -(1 + the number of active resizing threads).  Otherwise,
        * when table is null, holds the initial table size to use upon
        * creation, or 0 for default. After initialization, holds the
        * next element count value upon which to resize the table.
        */

翻译如下:

sizeCtl是控制标识符,不同的值表示不同的意义。

  • 负数代表正在进行初始化或扩容操作 ,其中-1代表正在初始化 ,-N 表示有N-1个线程正在进行扩容操作
  • 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,类似于扩容阈值。它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。实际容量>=sizeCtl,则扩容。

1、 transient volatile Node<K,V>[] table;是一个容器数组,第一次插入数据的时候初始化,大小是2的幂次方。这就是我们所说的底层结构:”数组+链表(或树)”

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

3、private static final intDEFAULT_CAPACITY = 16;

4、static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // MAX_VALUE=2^31-1=2147483647

5、private static finalint DEFAULT_CONCURRENCY_LEVEL = 16;

6、private static final float LOAD_FACTOR = 0.75f;

7、static final int TREEIFY_THRESHOLD = 8; // 链表转树的阀值,如果table[i]下面的链表长度大于8时就转化为数

8、static final int UNTREEIFY_THRESHOLD = 6; //树转链表的阀值,小于等于6是转为链表,仅在扩容tranfer时才可能树转链表

9、static final int MIN_TREEIFY_CAPACITY = 64;

10、private static final int MIN_TRANSFER_STRIDE = 16;

11、private static int RESIZE_STAMP_BITS = 16;

12、private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; // help resize的最大线程数

13、private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

14、static final int MOVED = -1; // hash for forwarding nodes(forwarding nodes的hash值)、标示位

15、static final int TREEBIN = -2; // hash for roots of trees(树根节点的hash值)

16、static final int RESERVED = -3; // hash for transient reservations(ReservationNode的hash值)

3、ConcurrentHashMap的构造函数

和往常一样,我们还是从类的构造函数开始说起。

    /**
     * Creates a new, empty map with the default initial table size (16).
     */
    public ConcurrentHashMap() {
    }

    public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }

    /*
     * Creates a new map with the same mappings as the given map.
     *
     */
    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        putAll(m);
    }

    public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, 1);
    }
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        if (initialCapacity < concurrencyLevel)   // Use at least as many bins
            initialCapacity = concurrencyLevel;   // as estimated threads
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }

有过HashMap和Hashtable源码经历,看这些构造函数是不是相当easy哈。

上面的构造函数主要干了两件事:

1、参数的有效性检查

2、table初始化的长度(如果不指定默认情况下为16)。

这里要说一个参数:concurrencyLevel,表示能够同时更新ConccurentHashMap且不产生锁竞争的最大线程数。默认值为16,(即允许16个线程并发可能不会产生竞争)。为了保证并发的性能,我们要很好的估计出concurrencyLevel值,不然要么竞争相当厉害,从而导致线程试图写入当前锁定的段时阻塞。

ConcurrentHashMap类中相关节点类:Node/TreeNode/TreeBin

1、Node类

Node类是table数组中的存储元素,即一个Node对象就代表一个键值对(key,value)存储在table中。

Node类是没有提供修改入口的(唯一的setValue方法抛异常),因此只能用于只读遍历。

此类的具体代码如下:

    /*
     *Node类是没有提供修改入口的(setValue方法抛异常,供子类实现),
     即是可读的。只能用于只读遍历。
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;//volatile,保证可见性
        volatile Node<K,V> next;

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

        public final K getKey()       { return key; }
        public final V getValue()     { return val; }
        /*
            HashMap中Node类的hashCode()方法中的代码为:Objects.hashCode(key) ^ Objects.hashCode(value)
            而Objects.hashCode(key)最终也是调用了 key.hashCode(),因此,效果一样。写法不一样罢了
        */;
        public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
        public final String toString(){ return key + "=" + val; }
        public final V setValue(V value) { // 不允许修改value值,HashMap允许
            throw new UnsupportedOperationException();
        }
        /*
             HashMap使用if (o == this),且嵌套if;ConcurrentHashMap使用&& 
             个人觉得HashMap格式的代码更好阅读和理解
        */
        public final boolean equals(Object o) {
            Object k, v, u; Map.Entry<?,?> e;
            return ((o instanceof Map.Entry) &&
                    (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                    (v = e.getValue()) != null &&
                    (k == key || k.equals(key)) &&
                    (v == (u = val) || v.equals(u)));
        }

        /*
         * Virtualized support for map.get(); overridden in subclasses.
         *增加find方法辅助get方法  ,HashMap中的Node类中没有此方法
         */
        Node<K,V> find(int h, Object k) {
            Node<K,V> e = this;
            if (k != null) {
                do {
                    K ek;
                    if (e.has
  • 30
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 28
    评论
ConcurrentHashMapJava线程安全的哈希表实现,它在JDK 1.8进行了一些优化和改进。下面是ConcurrentHashMap在JDK 1.8的实现原理的简要说明: 1. 分段锁:ConcurrentHashMap将整个哈希表分成了多个段,每个段都有自己的锁。这样可以降低并发访问时的锁竞争,提高并发性能。默认情况下,ConcurrentHashMap的段数与CPU核心数相等。 2. 数组+链表/红黑树:每个段内部维护一个哈希桶数组,每个桶可以存储一个链表或者红黑树,用于解决哈希冲突。JDK 1.8对桶的实现进行了优化,当链表长度超过一定阈值(默认为8)时,会将链表转换为红黑树,提高查找、插入和删除操作的效率。 3. CAS+Synchronized:ConcurrentHashMap使用CAS(Compare and Swap)操作实现了线程安全的并发访问。它通过CAS操作来更新桶数组的元素,并使用Synchronized来保证线程安全性。 4. 锁分离:JDK 1.8ConcurrentHashMap引入了锁分离的概念,即读操作不需要获取锁,写操作需要获取锁。这样可以提高并发读的性能,多个线程可以同时读取不同的段。 5. 原子操作:ConcurrentHashMap在JDK 1.8引入了一些原子操作,比如putIfAbsent、remove、replace等,这些操作可以保证多个线程同时访问时的原子性。 总的来说,ConcurrentHashMap通过分段锁、数组+链表/红黑树、CAS+Synchronized等技术实现了线程安全的并发访问。它在JDK 1.8进行了优化和改进,提高了并发性能和吞吐量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 28
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值