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

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

数据结构

      ConcurrentHashMap 1.8 抛弃了Segment分段锁机制,采用Node + CAS + Synchronized来保证并发安全进行实现,采用table数组+链表+红黑树的存储结构。table数组元素作为锁,利用CAS+Synchronized来保证并发更新的安全从而实现了对每个数组元素(Node)进行加锁进一步减少并发冲突的概率。结构如下:

      注:1、对于个数超过8(默认值)的列表,jdk1.8中采用了红黑树的结构,那么查询的时间复杂度可以降低到O(logN),可以改进性能。
      2、新增字段 transient volatile CounterCell[] counterCells可方便的计算hashmap中所有元素的个数,性能大大优于jdk1.7中的size()方法。
      3、Unsafe.getObjectVolatile可以直接获取指定内存的数据,保证了每次拿到数据都是最新的。
      4、在JDK1.8中,仅在构造器中确保初始容量>=concurrentLevel,为兼容旧版本而保留;

重要属性

      baseCount:元素的个数,当插入新数据或则删除数据时,会通过addCount()方法更新baseCount

      sizeCtl:hash表初始化或扩容时的一个控制位标识量。
            负数代表正在进行初始化或扩容操作。
            -1代表正在初始化
            -N 表示有N-1个线程正在进行扩容操作  
            正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小,这一点类似于扩容阈值的概念。它的值始终是当前ConcurrentHashMap容量的0.75倍,这与loadfactor是对应的。

重要类

      Node :最核心的内部类,它包装了key-value键值对,所有插入ConcurrentHashMap的数据都包装在这里面。它与HashMap中的定义很相似,但是但是有一些差别它对value和next属性设置了volatile同步锁(与JDK7的Segment相同),它不允许调用setValue方法直接改变Node的value域,它增加了find方法辅助map.get()方法。

      TreeNode:树节点类,另外一个核心的数据结构。当链表长度过长的时候,会转换为TreeNode。但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。而且TreeNode在ConcurrentHashMap集成自Node类,而并非HashMap中的集成自LinkedHashMap.Entry<K,V>类,也就是说TreeNode带有next指针,这样做的目的是方便基于TreeBin的访问。

      TreeBin:这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。在构造TreeBin节点时,仅仅指定了它的hash值为TREEBIN常量,这也就是个标识。

      ForwardingNode:一个用于连接两个table的节点类。它包含一个nextTable指针,用于指向下一张表。而且这个节点的key value next指针全部为null,它的hash值为-1. 这里面定义的find的方法是从nextTable里进行查询节点,而不是以自身为头节点进行查找。其中存储nextTable的引用。只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动。

核心方法

      tabAt : 获得在i位置上的Node节点

      casTabAt:利用CAS算法设置i位置上的Node节点。之所以能实现并发是因为他指定了原来这个节点的值是多少:在CAS算法中,会比较内存中的值与你指定的这个值是否相等,如果相等才接受你的修改,否则拒绝你的修改,因此当前线程中的值并不是最新的值,这种修改可能会覆盖掉其他线程的修改结果  有点类似于SVN

      setTabAt:利用volatile方法设置节点位置的值

/* ---------------- Table element access -------------- */

/*
 * Volatile access methods are used for table elements as well as
 * elements of in-progress next table while resizing.  All uses of
 * the tab arguments must be null checked by callers.  All callers
 * also paranoically precheck that tab's length is not zero (or an
 * equivalent check), thus ensuring that any index argument taking
 * the form of a hash value anded with (length - 1) is a valid
 * index.  Note that, to be correct wrt arbitrary concurrency
 * errors by users, these checks must operate on local variables,
 * which accounts for some odd-looking inline assignments below.
 * Note that calls to setTabAt always occur within locked regions,
 * and so in principle require only release ordering, not
 * full volatile semantics, but are currently coded as volatile
 * writes to be conservative.
 */

@SuppressWarnings("unchecked")
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
	return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

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

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
	U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

put操作

      利用spread方法对key的hashcode进行一次hash计算,来确定这个值在table中的位置

  1. 如果相应位置的Node还未初始化,则通过CAS插入相应的数据;
  2. 如果相应位置的Node不为空,且当前该节点不处于移动状态,则对该节点加synchronized锁,
  3. 如果该节点的hash>0,则得到的结点就是hash值相同的节点组成的链表的头节点则遍历链表更新节点或向后遍历,直到链表尾插入新节点。
  4. 如果该节点是TreeBin类型的节点,说明是红黑树结构,则调用红黑树的插值方法putTreeVal插入新节点;
  5. 如果binCount不为0,说明put操作对数据产生了影响,如果当前链表的个数达到8个,则通过treeifyBin方法转化为红黑树,如果oldVal不为空,说明是一次更新操作,没有对元素个数产生影响,则直接返回旧值;
  6. 如果插入的是一个新节点,则执行addCount()方法尝试更新元素个数baseCount
    /**
     * Maps the specified key to the specified value in this table.
     * Neither the key nor the value can be null.
     *
     * <p>The value can be retrieved by calling the {@code get} method
     * with a key that is equal to the original key.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with {@code key}, or
     *         {@code null} if there was no mapping for {@code key}
     * @throws NullPointerException if the specified key or value is null
     */
    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;
    }

get操作

      1、计算hash 值 根据 hash 值找到数组对应位置: (n – 1) & h

      2、根据该位置处节点性质进行相应查找

            如果该位置为 null,那么直接返回 null 就可以了

            如果该位置节点key与传入的key相同且不为null,直接返回value值。

            如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树,find查找返回value值

            如果是链表,进行遍历返回value值。

/**
 * Returns the value to which the specified key is mapped,
 * or {@code null} if this map contains no mapping for the key.
 *
 * <p>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) {
	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;
}

size()操作

  1. 初始化时counterCells为空,在并发量很高时,如果存在两个线程同时执行CAS修改baseCount值,则失败的线程会继续执行方法体中的逻辑,使用CounterCell记录元素个数的变化;
  2. 如果CounterCell数组counterCells为空,调用fullAddCount()方法进行初始化,并插入对应的记录数,通过CAS设置cellsBusy字段,只有设置成功的线程才能初始化CounterCell数组
  3. 如果通过CAS设置cellsBusy字段失败的话,则继续尝试通过CAS修改baseCount字段,如果修改baseCount字段成功的话,就退出循环,否则继续循环插入CounterCell对象;

      1.8中元素个数保存baseCount中,部分元素的变化个数保存在CounterCell数组中,通过累加baseCountCounterCell数组中的数量,即可得到元素的总个数;

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

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

CAS算法

      CAS算法包含三个参数CAS(V, E, N),判断预期值E和内存旧值是否相同(Compare),如果相等用新值N覆盖旧值V(Swap),否则失败不会执行任何操作;当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,其他线程失败(失败线程不会被阻塞,而是被告知“失败”,可以继续尝试);
      CAS在硬件层面可以被编译为机器指令执行,因此性能高于基于锁占有方式实现线程安全;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章