HashMap邻居:ConcurrentHashMap

HashMap

在进入今天的主题之前先来讲讲大家都熟知的HashMap类:
HashMap是集合中的一个重要的类,继承自Map接口,Map和其他集合最大的不同点就在于它是以键值对的形式储存数据,HashMap在开发中一个很致命的问题在于它的线程不安全性,在多线程下进行put()方法时有可能造成链表的闭环,从而形成死循环,然后就需要找寻线程安全的类。

HashTable

接下来看一下HashTable类,它是线程安全的,而它的线程安全操作是在所有涉及到多线程操作的地方都加上了synchronized关键字将整个集合锁住,在保证线程安全的同时这种方法会使得效率非常低下。

那有没有既能保持线程的安全性的同时,又能够使得效率更高呢

ConcurrentHashMap

分段锁机制

ConcurrentHashMap在最开始的时候(jdk1.7之前),为了同时解决安全和效率两大问题,采用了分段锁机制来储存数据,具体操作是:在集合对象中储存一个segment数组,把集合的元素分为16个分段,每个分段上储存一个HashEntry数组及对应的链表,每个HashEntry数组就相当于一个HashMap
分段锁
当触发锁时每一个segment中都会有一个独立的锁,而不会将整个对象锁住,所以多线程操作时每个segment中的数据不会相互影响,从而保证了效率
当然,这样储存的版本还有着不少的问题,如最多并发只有16个,结构过于臃肿等等,随着版本的迭代,在jdk1.8时更改了ConcurrentHashMap的结构,使之更加完善

jdk1.8的ConcurrentHashMap

CAS原理

jdk1.8版本的ConcurrentHashMap与之前版本相较而言,一个较大的改变就是摈弃了segment数组,利用CAS原理解决并发问题,CAS原理也叫作比较交换原理,其中有三个数:所存的值V,预期的值A,要变换成为的新值B
当A和V相同时,V会修改成B,否则不进行操作。这样就能较好的解决线程安全的问题。

数据结构

在jdk1.8版本之后,ConcurrentHashMap是摒弃了segment数组的数据结构,而是采用了和HashMap差不多的数组+链表+红黑树的结构:
jdk1.8版本的ConcurrentHashMap
我们先来看一个简单的ConcurrentHashMap的例子:

import java.util.concurrent.ConcurrentHashMap;

public class DemoConcurrentHashMap {


    public static void main(String[] args) {
        ConcurrentHashMap<String,Integer> map = new ConcurrentHashMap<>();
        String key = "aaa";
        int value = 1;
        map.put(key,value);
        System.out.println(key + "--->" + map.get(key));
    }
}

运行结果为:
运行结果——ConcurrentHashMap
可见,map常用的put()和get()方法在ConcurrentHashMap中实现起来和HashMap中似乎都一样,但是,进入源码中就能发现其中不同所在。

构造方法

首先看一下其构造方法:

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;
    }
 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;
    }

ConcurrentHashMap共有5个构造方法:
它的无参构造方法非常特别,是一个空实现,也并没有对集合进行初始化操作,那么它的初始化在哪里进行呢:put()方法中。这个奇怪的构造方法也是它和其他集合一大不同点所在。

第二种构造函数传入数组大小,并且将sizeCtl进行赋值,其取值是最小的大于容量的一个2的次幂数
sizeCtl是一个用来控制数组大小的属性,有以下几种情况:
为-1时:代表正在进行初始化操作
为-n时:表示有n-1个线程进行扩容
正数时:若数组未初始化则表示正在进行初始化操作,否则则表示可用容量

其他的构造方法主要传入的参数有Map集合,初始容量,装载因子,线程数等,用法不多

put()方法

构造方法之后就应该进行传值了,和HashMap一样,也是用的put()方法:
源代码如下:

	public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        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)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

该代码的逻辑都是if……else非常好理解:
1、如果键或值为null,抛出空指针异常
2、若数组未初始化,则调用initTable()方法进行初始化,这里就解决了构造方法未初始化的问题
3、否则,如果新插入的键没有产生hash冲突的话,便直接进行CAS插入
4、产生了hash冲突的话,如果头结点的hash值为MOVED(-1)的话,则表示应该进行扩容操作
5、产生hash冲突且不用扩容的话,将会对Node数组中该位置进行加锁,防止其他线程的更改,并且根据该位置储存的结构进行插入结点,如果数据结构是链表,则添加至最后一个位置,如果是红黑树,则添加到它应该在的位置
6、到这里,插入操作已经完成,还需判断插入之后,Node数组的该位置是否需要改变数据结构,如果需要则用treeifyBin()方法将链表转换为红黑树
最后记录一下table中元素个数,便完成一次添加元素的操作

get()方法

说完了元素添加之后,我们聊一聊也是常用的用于查找的get()方法
源码如下:

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

和HashMap一样,ConcurrentHashMap的查找也需要先得到key的hash值,获取方法是spread()方法:

static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }

该方法将key的hash值高16位与低16位异或后再进行hash,两次hash减少hash冲突
计算出hash值后,定位到table上的对应位置
1、如果key是链表首节点就直接返回所对应的值
2、若Node节点的hash值小于0时,表示正在扩容,则调用正在扩容节点的find
方法查找
3、若都不成立,则搜寻链表

size()方法

看完put/get后,我们看一下另一个常用的方法:size
同样,我们先看它的源码:

public int size() {
        long n = sumCount();
        return ((n < 0L) ? 0 :
                (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
                (int)n);
    }

可以看出,就一个重要的sumCount()方法,看一下它的源码:

 @sun.misc.Contended static final class CounterCell {
        volatile long value;
        CounterCell(long x) { value = x; }
    }

    final long sumCount() {
        CounterCell[] as = counterCells; CounterCell a;
        long sum = baseCount;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    sum += a.value;
            }
        }
        return sum;
    }

size()方法用一个volatile类型的变量baseCount记录元素的个数,如在前面的put()方法中,有一个addCount()方法,便是用来增加元素的个数而CounterCell数组,作用则是在多并发环境下辅助记录元素个数,在使用size()方法时将baseCount中的元素和CounterCell数组中的每个元素进行累加,结果则为当前的所有元素个数。
虽然jdk1.7的ConcurrentHashMap和HashMap结构上截然不同,但到了1.8版本,可以看到二者在数据结构上是有一定相似的,最大的不同还是在于线程安全方面。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值