ConcurrentHashMap JDK1.7中结构原理及源码分析

注:本文根据网络和部分书籍整理基于JDK1.7书写,如有雷同敬请谅解  欢迎指正文中的错误之处。

数据结构

       ConcurrentHashMap是HashMap的一个线程安全的、支持高效并发的版本。在多线程并发场景下HashTable和由同步包装器包装的HashMap,都是通过使用一个全局的锁来同步不同线程间的并发访问,会带来不可忽视的性能问题。

      ConcurrentHashMap采用分段锁的设计Segment + HashEntry的方式进行实现,采用table数组+单向链表的数据结构结构如下:

ConcurrentHashMap JDK1.7中结构原理及源码分析_第1张图片

重要属性

      initialCapacity:初始容量(默认16),这个值指的是整个 ConcurrentHashMap 的初始容量,实际操作的时候需要平均分给每个 Segment。

      concurrencyLevel:并发数(Segment 数,默认16)。ConcurrentHashMap采用了分段锁的设计,分段锁个数即Segment[]的数组长度,只有在同一个分段内才存在竞态关系,不同的分段锁之间没有锁竞争,所以当它们分别操作不同的 Segment时最多可以同时支持16个线程并发。
      注:用户可以在构造函数中初始设置为其他值,当用户设置并发度时,ConcurrentHashMap会使用大于等于该值的最小2幂指数作为实际并发度(如用户设置并发度为17,实际并发度则为32),但是一旦初始化以后,它是不可以扩容的。
不能超过规定的最大值:
final int MAX_SEGMENTS = 1 << 16

      segmentsSegment 通过继承 ReentrantLock 来进行加锁,每次需要加锁的操作锁住的是一个 segment,每个 Segment 对象用来守护其(成员对象 table 中)包含的若干个桶,这样保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
      注:JDK7中除了第一个Segment之外,剩余的Segment采用的是延迟初始化的机制:每次put之前都需要检查key对应的Segment是否为null,如果是则调用ensureSegment()以确保对应的Segment被创建。Segment[i] 的默认大小为 2,负载因子是 0.75

      table: 由 HashEntry 对象组成的数组。table 数组的每一个数组成员就是散列映射表的一个桶。

      count计数器,它表示每个 Segment 对象管理的 table 数组(若干个 HashEntry 组成的链表)包含的 HashEntry 对象的个数。每一个 Segment 对象都有一个 count 对象来表示本 Segment 中包含的 HashEntry 对象的总数。
      注:之所以在每个 Segment 对象中包含一个计数器,而不在 ConcurrentHashMap 中使用全局的计数器,是为了避免出现“热点域”而影响 ConcurrentHashMap 的并发性。需要更新计数器时,不用锁定整个 ConcurrentHashMap。

      loadFactor:负载因子(默认0.75),之前我们说了,Segment 数组不可以扩容,所以这个负载因子是给每个 Segment 内部使用的。

put操作

      1、判断value是否为null,如果为null,直接抛出异常。注:不允许key或者value为null

      2、通过哈希算法定位到Segment(key通过一次hash运算得到一个hash值,将得到hash值向右按位移动segmentShift位,然后再与segmentMask做&运算得到segment的索引j)。

      3、使用Unsafe的方式从Segment数组中获取该索引对应的Segment对象

      4、向这个Segment对象中put值

      注:对共享变量进行写入操作为了线程安全,在操作共享变量时必须得加锁,持有段锁(锁定整个segment)的情况下执行的。修改数据是不能并发进行的

      判断该值的插入是否会导致该 segment 的元素个数超过阈值,以确保容量不足时能够rehash扩容,再插值

      注:rehash 扩容 segment 数组不能扩容,扩容的是 segment 数组某个位置内部的数组 HashEntry[] 扩容为原来的 2 倍。先进行扩容,再插值

      查找是否存在同样一个key的结点,存在直接替换这个结点的值。否则创建一个新的结点并添加到hash链的头部,修改modCount和count的值,修改count的值一定要放在最后一步。

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
	HashEntry node = tryLock() ? null : scanAndLockForPut(key, hash, value);
	V oldValue;
	try {
		HashEntry[] tab = table;
		int index = (tab.length - 1) & hash;
		HashEntry first = entryAt(tab, index);
		for (HashEntry e = first;;) {
			if (e != null) {
				K k;
				if ((k = e.key) == key ||
					(e.hash == hash && key.equals(k))) {
					oldValue = e.value;
					if (!onlyIfAbsent) {
						e.value = value;
						++modCount;
					}
					break;
				}
				e = e.next;
			}
			else {
				if (node != null)
					node.setNext(first);
				else
					node = new HashEntry(hash, key, value, first);
				int c = count + 1;
				if (c > threshold && tab.length < MAXIMUM_CAPACITY)
					rehash(node);
				else
					setEntryAt(tab, index, node);
				++modCount;
				count = c;
				oldValue = null;
				break;
			}
		}
	} finally {
		unlock();
	}
	return oldValue;
}

get操作

    1、计算 hash 值,找到 segment 数组中的具体位置,使用Unsafe获取对应的Segment

    2、根据 hash 找到数组中具体的位置

    3、从链表头开始遍历整个链表(因为Hash可能会有碰撞,所以用一个链表保存),如果找到对应的key,则返回对应的value值,否则返回null。

    注:get操作不需要锁由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。

/**
 * Returns the value to which the specified key is mapped,
 * or {@code null} if this map contains no mapping for the key.
 *
 * 

More formally, if this map contains a mapping from a key * {@code k} to a value {@code v} such that {@code key.equals(k)}, * then this method returns {@code v}; otherwise it returns * {@code null}. (There can be at most one such mapping.) * * @throws NullPointerException if the specified key is null */ public V get(Object key) { Segment s; // manually integrate access methods to reduce overhead HashEntry[] tab; int h = hash(key); long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { for (HashEntry e = (HashEntry) UNSAFE.getObjectVolatile (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE); e != null; e = e.next) { K k; if ((k = e.key) == key || (e.hash == h && key.equals(k))) return e.value; } } return null; }

size()操作

     size操作需要遍历所有的Segment才能算出整个Map的大小。先采用不加锁的方式,循环所有的Segment(通过Unsafe的getObjectVolatile()以保证原子读语义)连续计算元素的个数,最多计算3次:

      1、如果前后两次计算结果相同,则说明计算出来的元素个数是准确的;
     
2、如果前后两次计算结果都不同,则给每个Segment进行加锁,再计算一次元素的个数;
     注:在put , remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。

/**
 * Returns the number of key-value mappings in this map.  If the
 * map contains more than Integer.MAX_VALUE elements, returns
 * Integer.MAX_VALUE.
 *
 * @return the number of key-value mappings in this map
 */
public int size() {
	// Try a few times to get accurate count. On failure due to
	// continuous async changes in table, resort to locking.
	final Segment[] segments = this.segments;
	int size;
	boolean overflow; // true if size overflows 32 bits
	long sum;         // sum of modCounts
	long last = 0L;   // previous sum
	int retries = -1; // first iteration isn't retry
	try {
		for (;;) {
			if (retries++ == RETRIES_BEFORE_LOCK) {
				for (int j = 0; j < segments.length; ++j)
					ensureSegment(j).lock(); // force creation
			}
			sum = 0L;
			size = 0;
			overflow = false;
			for (int j = 0; j < segments.length; ++j) {
				Segment seg = segmentAt(segments, j);
				if (seg != null) {
					sum += seg.modCount;
					int c = seg.count;
					if (c < 0 || (size += c) < 0)
						overflow = true;
				}
			}
			if (sum == last)
				break;
			last = sum;
		}
	} finally {
		if (retries > RETRIES_BEFORE_LOCK) {
			for (int j = 0; j < segments.length; ++j)
				segmentAt(segments, j).unlock();
		}
	}
	return overflow ? Integer.MAX_VALUE : size;
}

 

你可能感兴趣的:(Java)