4.1 java并发容器 - concurrentHashMap

一、HashMap为什么是线程不安全的

问题主要出现是hashmap的扩容操作的rehash操作上。

void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                //下面两行代码我们可以看出在rehash的时候是通过头插法插入到table中的
                e.next = newTable[i];
                //以下分析都假设在并发时线程A在此刻被挂起
                newTable[i] = e;
                e = next;
            }
        }
    }

1、hashmap会形成死循环,环形链表
假设容器初始值为如下图、hash的算法简单的使用取模操作
在这里插入图片描述
线程A和B并发向容器中put元素,发现容器使用率已经超过了容器的个人乘以加载因子的值,则需要扩容。

线程A在执行到上述代码时候时间片结束,此时A的结构为
在这里插入图片描述
此时线程B获取时间片,进行操作,并且扩容完成。
在这里插入图片描述
然后线程A再获取时间片来进行执行,由java内存模型可知道,newTable和table中的链表都是最新的值,A执行完成一轮循环后的结构为。
在这里插入图片描述
继续第二次循环
在这里插入图片描述
此时主存中的7的next是3,此时再将3rehash到table中,此时e已经为nulll,循环结束,就会出现如下结构
在这里插入图片描述之后涉及到轮询table3的结构时就会发生死循环操作

数据丢失的问题只要将初始化的结构该为7-》5-》3。最终resize后的结构是如下图,有兴趣的可以自己分析下。出现了环形链表和丢失了3
在这里插入图片描述

二、concurrentHashMap

JDK1.7的实现

1、concurrentHashMap的数据结构

在这里插入图片描述
segment可以看作是一把可重入锁,因为它继承了ReentrantLock,也就是所谓的分段锁,这里是在为并发时候做提高性能使用的。

hashEntry的定义是用volatile关键字修饰的,则可以保障他的内存可见性,线程间可以即使看到修改的数据。

static final class HashEntry<K,V> { 
            final K key;                 // 声明 key 为 final 型
            final int hash;              // 声明 hash 值为 final 型 
            volatile V value;           // 声明 value 为 volatile 型
            final HashEntry<K,V> next;  // 声明 next 为 final 型
            HashEntry(K key, int hash, HashEntry<K,V> next, V value)  { 
                this.key = key; 
                this.hash = hash; 
                this.next = next; 
                this.value = value; 
            } 
     }
2、初始化做了哪些事情
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> 
           implements ConcurrentMap<K, V>, Serializable { 
     
       //默认的segment的大小
       static final     int DEFAULT_INITIAL_CAPACITY= 16; 
    
       //加载因子,当table的占用个数大于table的容量乘以当前值的时候要触发扩容,进行rehash
       static final float DEFAULT_LOAD_FACTOR= 0.75f; 
     
       // 散列表的默认并发级别为 16。该值表示当前更新线程的估计并发量
       static final int DEFAULT_CONCURRENCY_LEVEL= 16; 
     
       /** 
        * segments 的掩码值
        * key 的散列码的高位用来选择具体的 segment 
        * 初始化的时候取的是
        */ 
       final int segmentMask; 
     
       /** 
        * 偏移量
        */ 
       final int segmentShift; 
     
       /** 
        * 由 Segment 对象组成的数组
        */ 
       final Segment<K,V>[] segments; 
     
       /** 
        * 创建一个带有指定初始容量、加载因子和并发级别的新的空映射。
        */ 
       public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { 
           if(!(loadFactor > 0) || initialCapacity < 0 || 
    concurrencyLevel <= 0) 
               throw new IllegalArgumentException(); 
     
     		//seghment的大小不能超过65535
           if(concurrencyLevel > MAX_SEGMENTS) 
               concurrencyLevel = MAX_SEGMENTS; 
     
           // 寻找最佳匹配参数(不小于给定参数的最接近的 2 次幂) 
           int sshift = 0; 
           int ssize = 1; 
           while(ssize < concurrencyLevel) { 
               ++sshift; 
               ssize <<= 1; 
           } 
           //此时ssize算下来等于16因为将1左移了4位
           //sshift等于4
           segmentShift = 32 - sshift;       // 偏移量值等于32-4=28这里是在put操作时候取用的是hash值的前3位来进行的定位segment位置
           segmentMask = ssize - 1;           // 掩码值,等于15,因为当前值要
           this.segments = Segment.newArray(ssize);   // 创建数组
     
           if (initialCapacity > MAXIMUM_CAPACITY) 
               initialCapacity = MAXIMUM_CAPACITY; 
           int c = initialCapacity / ssize; 
           if(c * ssize < initialCapacity) 
               ++c; 
           //table的个数是2
           int cap = 1; 
           while(cap < c) 
               cap <<= 1; 
           // 依次遍历每个数组元素
           for(int i = 0; i < this.segments.length; ++i) 
               // 初始化每个数组元素引用的 Segment 对象
    this.segments[i] = new Segment<K,V>(cap, loadFactor); 
       } 
     
       /** 
        * 创建一个带有默认初始容量 (16)、默认加载因子 (0.75) 和 默认并发级别 (16) 
     * 的空散列映射表。
        */ 
       public ConcurrentHashMap() { 
           // 使用三个默认参数,调用上面重载的构造函数来创建空散列映射表
    this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL); 
    }

总结下初始化做的事情

  • 计算出segment的掩码值=15,以为对segment定位的时候是按照%的方式进行操作的,使用的方式是利用位运算&的方式,因为a&2^n = a&2的n次方减1,2的4次方等于16.对16取余就相当于对2的4次方减1=15按位&的操作,这里一会get\put的时候会用到
  • 计算出cap的值=2,cap的含义就是table中的数组的个数
  • 初始化segment
3、get操作、怎么定位、如何保证线程安全、get方法的弱一致性
 1    public V get(Object key) {
 2        Segment<K,V> s; // manually integrate access methods to reduce overhead
 3        HashEntry<K,V>[] tab;
 4        int h = hash(key);
 5        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
 6        if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
 7            (tab = s.table) != null) {
 8            for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
 9                     (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
10                 e != null; e = e.next) {
11                K k;
12                if ((k = e.key) == key || (e.hash == h && key.equals(k)))
13                    return e.value;
14            }
15        }
16        return null;
17    }

  • 通过获取到hash的值,如果是自定义对象的话,会要求重写hashCode方法,hash中是先取到对象的hash值,再进行一个wang jenkis算法的再哈希。获取到hash值后先先右移segmentShift=28位,取前三位和segmentmask进行按位&运算得到segment的下标
  • 再对segmet中的table进行也进行按位&的操作获取到table的下标
  • 然后去遍历链表,获取到对应的值

如何保证线程安全:开始我们看到使用volatile修饰了hashEntry,即保证了线程之间的内存可见性,线程A改变之后不会进行缓存,直接会回写到内存当中,保证了线程之间的数据可见。

get方法的弱一致性:我们对hashEntry的可见性有了保证但是如果hashEntry已经进行了扩容and rehash则我们查询的还是旧的链表,则会出现get到的数据还是旧数据,这就是get方法的弱一致性。

4、put操作、怎么定位、如何保证线程安全、key相同是否会覆盖,那种方法会覆盖。
 1    public V put(K key, V value) {
 2        Segment<K,V> s;
 3        if (value == null)
 4            throw new NullPointerException();
 5        int hash = hash(key);
 		  //获取segment的位置
 6        int j = (hash >>> segmentShift) & segmentMask;
 7        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
 8             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
              //如果segmengt还未被初始化,则此处进行一个初始化动作
 9            s = ensureSegment(j);
          //执行put元素的操作
10        return s.put(key, hash, value, false);
11    }
 1        final V put(K key, int hash, V value, boolean onlyIfAbsent) {
              //首先对segmnet进行一个加锁操作
 2            HashEntry<K,V> node = tryLock() ? null :
 3                scanAndLockForPut(key, hash, value);
 4            V oldValue;
 5            try {
 6                HashEntry<K,V>[] tab = table;
                  //定位segment中的table中的下标
 7                int index = (tab.length - 1) & hash;
 8                HashEntry<K,V> first = entryAt(tab, index);
 9                for (HashEntry<K,V> e = first;;) {
10                    if (e != null) {
11                        K k;
                          //如果hash值相同,key也相同,根据是否需要覆盖的标识onlyIfAbsent来进行操作,覆盖的话直接将数据更新,不覆盖的话直接返回旧值
12                        if ((k = e.key) == key ||
13                            (e.hash == hash && key.equals(k))) {
14                            oldValue = e.value;
15                            if (!onlyIfAbsent) {
16                                e.value = value;
17                                ++modCount;
18                            }
19                            break;
20                        }
21                        e = e.next;
22                    }
23                    else {
24                        if (node != null)
25                            node.setNext(first);
26                        else
27                            node = new HashEntry<K,V>(hash, key, value, first);
28                        int c = count + 1;
29                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
							  //如果当前的table使用已经超过数组大小乘以加载因子则进行扩容和rehash
30                            rehash(node);
31                        else
32                            setEntryAt(tab, index, node);
33                        ++modCount;
34                        count = c;
35                        oldValue = null;
36                        break;
37                    }
38                }
39            } finally {
40                unlock();
41            }
42            return oldValue;
43        }

  • 定位segment位置并加锁
  • 定位table中的index
  • 去链表中查找,根据hashCode和key和onlyIfAbsent来判断是否需要覆盖旧值,代码中有详细注释
  • 如果当前的table使用已经超过数组大小乘以加载因子则进行扩容和rehash,并插入要插入的数据
  • 解锁

JDK1.8实现

1、1.8和1.7之间的变化
  • 取消了segment数据,锁的粒度直接作用在table上,减少了并发冲突的概率
  • 存储数据用链表+红黑树的的形式,红黑树的查找速度是log(n),性能很快,。但是插入操作需要进行红黑树的平衡调整,所以在8个元素以内使用链表的形式,8个元素以上使用红黑树的存储方式
2.主要数据结构和关键变量
  • Node和1.7中的hashEntry基本一致,是存储链表时的节点数据
  • sizeCtl
    负数:表示正在初始化或者扩容,-1表示正在初始化、-N表示正在有N个线程进行扩容
    正数:0表示还没有被初始化,N表示初始化或者下一次扩容的阈值
  • TreeNode 红黑树节点
  • TreeBin放在table中数据,也就是红黑树的头节点

3、操作的剖析,都做了哪些事情。

3、初始化
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;
    }
//算法的功能为将你输入的数字转换为距离最近的2的幂次方的正数
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;
    }

只是给成员变量赋值,put时进行实际数组的填充

4、get()方法
public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        //对hash值进行再散列,使得散列值更均衡
        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;
            }
            //table中存储的是红黑树,需要去红黑树中进行查找
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            //table中存储的是链表,遍历链表进行查找
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }
5、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();
        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)
            	//如果第一次put需要对容器进行初始化
                tab = initTable();
            //如果当前table中没有元素,则直接将数据插入到当前table的当前位置中
            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
            }
            //当前线程检测到容易正在扩容,去帮助进行扩容,所做的事情就是将数据进行重新rehash并且搬数据
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //对当前table进行加锁
                synchronized (f) {
                	//如果当前table存储的是链表。则将数据插入到链表中,和1.7操作类似
                    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) {
                	//如果当前table中存储的是链表,并且数据已经超过了8则进行链表到红黑树的转化
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

初始化方法

private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
        	//如果有其他线程正在初始化,则将当前线程让出cpu
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            //循环操作,使用CAS进行设置sizeCtl为-1
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        //数组的初始化
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        //sc的值设置为0.75n
                        sc = n - (n >>> 2);
                    }
                } finally {
                	//设置下一次需要扩容的阈值
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

这里需要注意下,在扩容的过程中,如果rehash后的table下的数据小于链表于红黑树的转化值(6),则需要将红黑树转化为链表,1.7版本hash的算法是将再散列后值的高位来进行和segmnet的个数减一进行&操作,1.8是用再散列后的值的高16位和tables的大小进行异或操作。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值