JUC知识点总结(六)多线程里的HashMap与ConcurrentHashMap源码分析

12. 多线程中的HashMap

12.1 HashMap不安全举例

  • Jdk1.7 头插法,多线程扩容时导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,同时也会出现数据丢失的问题。

  • Jdk1.8 尾插法,多线程put时会造成数据丢失。

12.2 HashTable与HashMap的区别

  • HashTable的底层数组初始大小为11,HashMap要求其必须为 2 n 2^{n}
  • HashTable通过取模求Hash值,HashMap通过位运算求,效率高;
  • Hash Map底层用数组+链表+红黑树,HashTable用数组+链表;
  • HashTable对HashMap中线程不安全的方法加了Synchronized,但效率低;

12.3 HashTable与Collections.synchronizedMap比较

  • 默认 Hashtable 和 synchrnizedMap 都是锁 类实例,synchrnizedMap 可以选择锁其他的 Object(mutex)

  • Hashtable 的 synchronized 是方法级别的;synchrnizedMap 的 synchronized 的代码块级别的

  • 两者性能相近,但是 synchrnizedMap 可以用 null 作为 key 和 value

12.4 JDK1.7中的ConcurrentHashMap

由Segment数组结构和HashEntry数组结构组成。Segment继承ReentrantLock。

重要变量

static final int DEFAULT_INITIAL_CAPACITY = 16;//默认初始容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认负载因子
static final int DEFAULT_CONCURRENCY_LEVEL = 16;//默认并发数量,会影响segments数组的长度(初始化后不能修改)
static final int MAXIMUM_CAPACITY = 1 << 30; //map最大容量
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;// 每个segment中HashEntry[]默认容量
static final int MAX_SEGMENTS = 1 << 16; //最大并发数量
static final int RETRIES_BEFORE_LOCK = 2; //非锁定情况下调用size和contains方法的重试次数,避免由于table连续被修改导致无限重试
final int segmentMask; //计算segment位置的掩码
final int segmentShift; //用于算segment位置时,hash参与运算的位数
final Segment<K,V>[] segments; //segment数组

static final class HashEntry<K,V> {
  final int hash;
  final K key; 
  volatile V value;
  volatile HashEntry<K,V> next;
}
static final class Segment<K,V> extends ReentrantLock implements Serializable {  
  static final int MAX_SCAN_RETRIES =//对segment加锁时,在阻塞之前自旋的次数
 			      Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
  transient volatile HashEntry<K,V>[] table;  
  transient int count;
  transient int modCount;
  transient int threshold;// 当table大小超过阈值时扩容,值为(int)(capacity *loadFactor)
  final float loadFactor;//负载因子
}

(1)初始化

public ConcurrentHashMap(int initialCapacity,float loadFactor,int concurrencyLevel) {
	if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
		throw new IllegalArgumentException();
	if (concurrencyLevel > MAX_SEGMENTS)//并发等级不可大于最大并发度
		concurrencyLevel = MAX_SEGMENTS;
// 第一步,segments数组的长度ssize为大于等于concurrencyLevel的最小的2的最小次方数
	int sshift = 0;//ssize左移的次数
	int ssize = 1;//segment数组长度
	while (ssize < concurrencyLevel) { 
		++sshift; 
		ssize <<= 1;
	}
// 第二步,初始化segmentShift和segmentMask
	this.segmentShift = 32 - sshift; // 用于计算key的hash值参与运算位数
	this.segmentMask = ssize - 1; // 哈希运算的掩码,每位都是1
// 第三步,确定每个segemnt中HashEntry[]的长度
	if (initialCapacity > MAXIMUM_CAPACITY)
		initialCapacity = MAXIMUM_CAPACITY;
	int c = initialCapacity / ssize;// 计算每个segment中table的容量
	if (c * ssize < initialCapacity)
		++c;
 // HashEntry[]默认容量
	int cap = MIN_SEGMENT_TABLE_CAPACITY;
	while (cap < c)
		cap <<= 1;
	for (int i = 0; i < this.segments.length; ++i)
		this.segments[i] = new Segment<K,V>(cap, loadFactor); 
}

要点:确认ConcurrentHashMap的并发度,也就是Segment数组长度,并保证它是2的n次幂;确认HashEntry数组的初始化长度,并保证它是2的n次幂。

(2)定位Segment

final Segment<K,V> segmentFor(int hash) {
	return segments[(hash >>> segmentShift) & segmentMask];
}

要点:取哈希值的高4位参与运算,获得每个key值的定位

(3)get操作

public V get(Object key) {
	int hash = hash(key.hashCode());
	return segmentFor(hash).get(key, hash);
}

要点:get阶段不需要加锁,变量都可以保证可见性。

(4)put操作

put方法首先需要循环获取锁,获得锁后定位到Segment,然后在Segment里进行插入操作。插入操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置,然后将其放在HashEntry数组里。

  • **是否需要扩容:**在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阈值,则对数组进行扩容。值得一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。
  • **如何扩容:**在扩容的时候,首先会创建一个容量是原来容量两倍的数组,然后将原数组里的元素进行再散列后插入到新的数组里。为了高效,ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。

(5)size操作

先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。(modCount)

12.5 JDK1.8中的ConcurrentHashMap

重要常量

	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;

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

(1)初始化

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)put()

  • 如果没有初始化就先调用initTable()方法来进行初始化过程

  • 如果没有hash冲突就直接CAS插入

  • 如果还在进行扩容操作就先进行扩容

  • 如果存在hash冲突,就加锁(synchronized)来保证线程安全,遍历到尾端插入,或按照红黑树结构插入,

  • 若该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环

  • 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    //取hashCode
    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;
}

(2)get()

  • 计算hash值,定位到该table索引位置,如果是首节点符合就返回

  • 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回

  • 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null

public V get(Object key) {
    ConcurrentHashMap.Node<K, V>[] tab;
    ConcurrentHashMap.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;
}

12.6 两种实现方式的对比

JDK1.7:ReentrantLock+Segment+HashEntry, JDK1.8:synchronized+CAS+HashEntry+红黑树

JDK1.8的实现降低锁的粒度,JDK1.7版本锁的粒度是基于Segment的,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)

JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了;

JDK1.8使用红黑树来优化链表;

下一篇
JUC知识点总结(七)ConcurrentLinkedQueue知识点总结

发布了54 篇原创文章 · 获赞 11 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/dong_W_/article/details/105075344
今日推荐