ConcurrentHashMap

jdk1.7以前与jdk1.8

@since 1.5

jdk1.7以前的ConcurrentHashMap使用的是分段锁实现的并发

jdk1.8之后的版本使用的数组+链表+红黑树数据结构再加上CAS原子操作实现的

jdk1.7以前的实现

Hashtable因为在put等操作的时候使用的synchronized加锁的方式来实现并发安全,但是这样访问map就效率太过于低下了,所以就有了分段锁的方式。大概的意思就是,在开始访问Hashtable的时候因为加了锁的原因,每次只有一个线程可以对map进行修改,但是map太常用且违背map设计的初衷,所以Hashtable就渐渐的被遗弃了。

1.7以前的ConcurrentHashMap使用分段锁解决了这个并发效率低下的问题。分段锁意思大概就是对map的bucket采用分段的方式分成一个个段,再对每个段分别加锁,这样,在有线程访问某一段的时候就要获取某一段的锁,但是其他的段因为是使用的不同的锁,从而其他段是可以被其他线程访问的,这样就大大解决了map并发访问的问题。需要注意的是,ConcurrentHashMap既不允许key不为null,值也不允许为null;

ConcurrentHashMap是由Segment[] 与HashEntry[] 构成的,大概就是一个CurrentHashMap由多个segment构成,segment继承了ReentrantLock,而一个个segment又是由多个HashEntry构成的,HashEntry里保存着KV值。这样相当于一个Seg管理多个Entry。

从小到大看:

成员变量:

    /**
     * 默认初始容量
     */
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    /**
     * 默认加载因子
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 默认并发度,该参数会影响segments数组的长度
     */
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;

    /**
     * 最大容量,构造ConcurrentHashMap时指定的大小超过该值则会使用该值替换,
     * ConcurrentHashMap的大小必须是2的幂,且小于等于1<<30,以确保可使用int索引条目
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * 每个segment中table数组的最小长度,必须是2的幂,至少为2,以免延迟构造后立即调整大小
     */
    static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

    /**
     * 允许的最大segment数量,用于限定构造函数参数concurrencyLevel的边界,必须是2的幂
     */
    static final int MAX_SEGMENTS = 1 << 16; // slightly conservative

    /**
     * 非锁定情况下调用size和containsValue方法的重试次数,避免由于table连续被修改导致无限重试
     */
    static final int RETRIES_BEFORE_LOCK = 2;
    /**
     * 与当前实例相关联的,用于key哈希码的随机值,以减少哈希冲突
     */
    private transient final int hashSeed = randomHashSeed(this);

    private static int randomHashSeed(ConcurrentHashMap instance) {
        if (sun.misc.VM.isBooted() && Holder.ALTERNATIVE_HASHING) {
            return sun.misc.Hashing.randomHashSeed(instance);
        }
        return 0;
    }
    /**
     * 用于索引segment的掩码值,key哈希码的高位用于选择segment
     */
    final int segmentMask;

    /**
     * 用于索引segment偏移值
     */
    final int segmentShift;

    /**
     * segments数组
     */
    final Segment<K,V>[] segments;

首先看构造函数(有很多构造函数,但是最终都调用的这个):

@SuppressWarnings("unchecked")
    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();
        // 并发等级如果大于了最大的seg,就要将seg扩充到并发等级、
        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;
        // 寻找与给定参数concurrencyLevel匹配的最佳Segment数组ssize,必须是2的幂
        // 如果concurrencyLevel是2的幂,那么最后选定的ssize就是concurrencyLevel
        // 否则concurrencyLevel,ssize为大于concurrencyLevel最小2的幂值
        // concurrencyLevel为7,则ssize为2的3次幂,为8
      
        // 计算ssize的时候进行了多少次的移位运算
        int sshift = 0;
        //ssize = segment size
        int ssize = 1;
        //就是找到最符合并发等级的segment size
        while (ssize < concurrencyLevel) {
            ++sshift;
            //左移是乘2 ssize = 1;  会变成2的幂次
            ssize <<= 1;
        }
        //用于定位段
        this.segmentShift = 32 - sshift;
        // 因为ssize是2的幂次方,所以segmentMask是一个全1的一个二进制
        this.segmentMask = ssize - 1;
        
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        //计算每个Segment中,table数组的初始大小     总的桶数/总的段数=每个段包含的bucket的数量
        int c = initialCapacity / ssize;
        //确保每个桶都被分到了seg中    一个seg中包含多个table 一个table又包含一个hashentry[] 
        if (c * ssize < initialCapacity)
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;
        while (cap < c)
            cap <<= 1;
        // 创建segments和第一个segment
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); //原子按顺序写入segments[0]
        this.segments = ss;
    }
    /**
     *  map转化为ConcurrentHashMap
     *
     * @param m the map
     */
    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
                      DEFAULT_INITIAL_CAPACITY),
             DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
        putAll(m);
    }

hash算法

private int hash(Object k) {
        int h = hashSeed;
 
        if ((0 != h) && (k instanceof String)) {
            return sun.misc.Hashing.stringHash32((String) k);
        }
 
        h ^= k.hashCode();
 
        // Spread bits to regularize both segment and index locations,
        // using variant of single-word Wang/Jenkins hash.
        h += (h <<  15) ^ 0xffffcd7d;
        h ^= (h >>> 10);
        h += (h <<   3);
        h ^= (h >>>  6);
        h += (h <<   2) + (h << 14);
        return h ^ (h >>> 16);
    }

定位key所在的seg

private Segment<K,V> segmentForHash(int h) {
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        return (Segment<K,V>) UNSAFE.getObjectVolatile(segments, u);
    }

HashEntry:

static final class HashEntry<K,V> {
    //数组结构的一个个Entry
       final K key;                       
       final int hash;                   
       volatile V value;                
       final HashEntry<K,V> next;      
 
        HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
            this.key = key;
            this.hash = hash;
            this.next = next;
            this.value = value;
        }
 
        @SuppressWarnings("unchecked")
        static final <K,V> HashEntry<K,V>[] newArray(int i) {
        return new HashEntry[i];
        }
    }

jdk1.8的实现

利用CAS原子操作与synchronized保证并发

底层采用数组+链表+红黑树的实现

一些常量:

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;
private static int RESIZE_STAMP_BITS = 16;
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

一些成员变量:

transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
private transient volatile long baseCount;
//size controll
private transient volatile int sizeCtl;
private transient volatile int transferIndex;
private transient volatile int cellsBusy;
private transient volatile CounterCell[] counterCells;

构造函数:

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
    //size*loadFactor = capacity
    //size:那个数组的长度
    //capacity:
        long size = (long)(1.0 + (long)initialCapacity / loadFactor);
    //cap是大于等于size的一个2的次幂
        int cap = (size >= (long)MAXIMUM_CAPACITY) ?
            //tableSizeFor会返回大于等于size的2的次幂
            MAXIMUM_CAPACITY : tableSizeFor((int)size);
        this.sizeCtl = cap;
    }

private static final int tableSizeFor(int c) {
        int n = c - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

要摒弃jdk1.7对于CHM的想法。

CHM主要还是为了实现一个线程安全的HashMap,而线程安全主要是体现在了对于map的put与get操作上。所以我们主要看一下CHM的put与get。

注意几个变量:f代表Node,fh代表Node的hashcode。

其中,fh=-1代表此时数组正在扩容。fh=-2代表当前树根节点的hash。fh>0代表此时的node是链表node。

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()); //两次hash,减少hash冲突,可以均匀分布
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) { //对这个table进行迭代
        Node<K,V> f; int n, i, fh;
        //这里就是上面构造方法没有进行初始化,在这里进行判断,为null就调用initTable进行初始化,属于懒汉模式初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果i位置没有数据,就直接无锁插入
            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;
            //如果以上条件都不满足,那就要进行加锁操作,也就是存在hash冲突,锁住链表或者红黑树的头结点
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) { //表示该节点是链表结构
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //这里涉及到相同的key进行put就会覆盖原先的value
                            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) { //如果链表的长度大于8时就会进行红黑树的转换
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);//统计size,并且检查是否需要扩容
    return null;
}

put操作对于hashmap而言,主要是要先遍历数组,找到要put的位置,然后查看有无冲突,有冲突则放入链表/红黑树中,无冲突则直接放入。大概的逻辑是这样的,细节慢慢来。

我们看到在CHM中,对于put的主要逻辑体现在:

for (Node<K,V>[] tab = table;;) { //对这个table进行迭代
        Node<K,V> f; int n, i, fh;
        //这里就是上面构造方法没有进行初始化,在这里进行判断,为null就调用initTable进行初始化,属于懒汉模式初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果i位置没有数据,就直接无锁插入
            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;
            //如果以上条件都不满足,那就要进行加锁操作,也就是存在hash冲突,锁住链表或者红黑树的头结点
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) { //表示该节点是链表结构
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //这里涉及到相同的key进行put就会覆盖原先的value
                            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) { //如果链表的长度大于8时就会进行红黑树的转换
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);//统计size,并且检查是否需要扩容

分解看就是:

先遍历整个数组table:for (Node<K,V>[] tab = table;;)

然后这里需要注意的一点,这里使用了懒加载的方式,在第一次put的时候才会初始化map。

if (tab == null || (n = tab.length) == 0)
            tab = initTable();

初始化了之后,又分为几种情况插入:

1.如果要插入的位置没有元素,则直接插入:

else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//如果i位置没有数据,就直接无锁插入
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }

这里需要注意,使用了tabAt

@SuppressWarnings("unchecked") // ASHIFT等均为private static final  
    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { // 获取索引i处Node  
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);  
    }  

在这里查看此时要插入的地方有没有值得时候,才用了“安全”的方法。因为这里有可能出现多个线程查看某个位置是否有值得情况。需要保证数据的可见性,如果在线程A看到这个位置没有值,准备插入,但是线程B也看到了这个位置没有值,但是线程B在插入的时候线程A已经插入了值,这时候就会出现线程安全的问题。这里使用了Unsafe类的getObjectVolatile,从而保证了一个线程取node的时候另一个立刻可见,也就避免了上面说的线程安全的问题

在确保了这里没有值的时候,再使用cas操作插入某个值:

// 利用CAS算法设置i位置上的Node节点(将c和table[i]比较,相同则插入v)。  
    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);  
    }  

保证插入没有线程安全问题。

2.如果此时数组table正在扩容,则先进行扩容,因为这里是采用的无限循环的方式,所以在扩容完成之后,跳出此次循环之后还是会进入循环继续进行插入操作。

注意,这里查看数组是否正在进行扩容的方式。比较有意思,而我也没怎么理解。

3.要插入的地方有值,存在hash冲突。

这时候就要加锁了,防止多个线程同时操作时造成线程安全问题。

synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) { //表示该节点是链表结构
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //这里涉及到相同的key进行put就会覆盖原先的value
                            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;
                        }
                    }
                }
            }

这里对某一个结点加锁,保证一次只有一个线程可以进入。如果是链表结构(fh>0),则直接插入链表尾。如果是树结构,则红黑树旋转插入。最后查看书否需要将链表结构转化为树结构。

至于扩容操作自己也没看咋懂或者说好多看不下去了这里留一个todo

接下来看看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()); //计算两次hash
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {//读取首节点的Node元素
        if ((eh = e.hash) == h) { //如果该节点就是首节点就返回
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
        //查找,查找到就返回
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        while ((e = e.next) != null) {//既不是首节点也不是ForwardingNode,那就往下遍历
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

get方法比较简单,大概就是计算hash,在这里有可能碰到在取元素的时候map正在扩容,就调用find方法去查找元素。

最后,为什么要使用红黑树代替链表。

因为对于链表而言,要是链表长度太长,会出现遍历操作很慢的情况(链表的特点,插入删除快,遍历慢),所以采用了红黑树(平衡的二叉搜索树)。至于为什么不适用数组,是因为插入删除慢。。。。。。至于为什么不使用二叉搜索树。因为,二叉搜索树有可能出现很不平衡的状况,使得遍历的效率跟链表差不多。

本篇博客参考了https://www.cnblogs.com/study-everyday/p/6430462.html,如果大家要深入的去了解,建议看看这篇。

转载于:https://www.cnblogs.com/GaryZz/p/11341265.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值