ConcurrentHashMap源码查看

一、博客背景

因hashmap不是线程安全的,在多线程下插入会有死锁的问题,所以ConcurrentHashMap就出现来替代了,ConcurrentHashMap在java7和java8中数据结构有了变化,我们本篇博客会根据我们目录中的提到的问题一起交叉查看jvav7和java8的源码,查看之前可以自己先想想问题的答案。篇幅较多,请耐心看完,java7的版本号为1.7.0_04,java8的版本号为1.8.0_162

二、常见问题

  1. CurrentHashMap的实现原理
  2. CurrentHashMap如何实现put数据的
  3. CurrentHashMap如何实现获取数据的
  4. CurrentHashMap的扩容机制是什么
  5. CurrentHashMap如何实现线程安全的
  6. 为什么CurrentHashMap的读操作不加锁
  7. CurrentHashMap在jdk1.7和1.8中的区别
  8. JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock?
  9. ConcurrentHashMap能完全替代HashTable吗?

三、构造函数

1.java7

//系统成员变量如下
//在构造函数未指定初始大小时,默认使用的map大小
 
static final int DEFAULT_INITIAL_CAPACITY = 16;

//默认的扩容因子,当初始化构造器中未指定时使用。
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
 * 默认的并发度,这里所谓的并发度就是能同时操作ConcurrentHashMap的线程的最大数量,
 * 由于ConcurrentHashMap采用的存储是分段存储,即多个segement,加锁的单位为segment,所以一个ConcurrentHashMap的并行度就是segments数组的长度,
 * 故在构造函数里指定并发度时同时会影响到ConcurrentHashMap的segments数组的长度,因为数组长度必须是大于并行度的最小的2的幂。
 */
static final int DEFAULT_CONCURRENCY_LEVEL = 16;

//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//每个分段最小容量
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;

//分段最大的容量
static final int MAX_SEGMENTS = 1 << 16; 

//默认自旋次数,超过这个次数直接加锁
static final int RETRIES_BEFORE_LOCK = 2;

//用于索引segment的掩码值
final int segmentMask;

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

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

//HashEntry数据结构,类似HashMap中的Entry对象,是ConcurrentHashMap数据结构中最小的存储单元,它就是对应一个个的<k,v>节点
static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;

        HashEntry(int hash, K key, V value, HashEntry<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
 //代码略
}

//Segment成员变量 构成ConcurrentHashMap底层数组的对象
static final class Segment<K,V> extends ReentrantLock implements Serializable{
    
     //scanAndLockForPut中自旋循环获取锁的最大自旋次数。
    static final int MAX_SCAN_RETRIES =
        Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;

     //Segment对象内部的存储结构
    transient volatile HashEntry<K,V>[] table;

    /**
     * 元素的个数,这里没有加volatile修饰,所以只能在加锁或者确保可见性(如Unsafe.getObjectVolatile)的情况下进行访问,不然无法保证数据的正确性
     */
    transient int count;

    //segment元素修改次数记录,每次Remove、put都相当于一次修改,由于未进行volatile修饰,所以访问规则和count类似
    transient int modCount;

    //扩容指标
    transient int threshold;

    // 负载因子
    final float loadFactor;

   Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
	    this.loadFactor = lf;
	    this.threshold = threshold;
	    this.table = tab;
    }
    
   //代码略
}


//默认构造函数
public ConcurrentHashMap() {
	this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
//带map容量大小的构造函数
public ConcurrentHashMap(int initialCapacity) {
	this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
//带map容量大小,加载因子大小的构造函数
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
	this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}

/**
 * initialCapacity:初始参数
 * loadFactor:加载因子
 * concurrencyLevel:并发级别即Segment的数量
 */
public ConcurrentHashMap(int initialCapacity,
						 float loadFactor, int concurrencyLevel) {
	//非法数校验
	if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
		throw new IllegalArgumentException();
	传参传入的并发级别(segments数组的最大值)最大不能超过上面定义的常量,也就是2^16
    if (concurrencyLevel > MAX_SEGMENTS)
		concurrencyLevel = MAX_SEGMENTS;
	// 用来记录向左按位移动的次数
	int sshift = 0;
	//用来记录Segment的数量
	int ssize = 1;
	//该段while循环保证Segment的数量是2的幂,为何需要保持2的幂跟hashmap的容量大小为何保持2的幂原因一样,具体想了解的可以去看我写的hashmap源码查看的博客
	while (ssize < concurrencyLevel) {
		++sshift;
		ssize <<= 1;
	}
    segmentshift和segmentMask用来定位节点属于segments数组中哪个元素,也就是定位到table
	this.segmentShift = 32 - sshift;
	//这里也可以推断出Segment数量一旦确定不能在变,扩容是扩Segment数组内的HashEntry数组
	this.segmentMask = ssize - 1;
	if (initialCapacity > MAXIMUM_CAPACITY)
		initialCapacity = MAXIMUM_CAPACITY;
	// //c为每个Segment数组内要放置多少个HashEntry数组
	int c = initialCapacity / ssize;
	//确保无余数
	if (c * ssize < initialCapacity)
		++c;
	//确保每个Segment内部的HashEntry数组的大小一定为2的幂(原因与前面一致),当三个参数皆为默认值时,其Segment内部的table大小是2,
	int cap = MIN_SEGMENT_TABLE_CAPACITY;
	while (cap < c)
		cap <<= 1;
	// create segments and segments[0]
	//初始化Segment数组,并填充Segment[0],阈值是(int)(cap * loadFactor),当参数皆为默认时,该值为1,当put第一个元素时不会扩容,在put就会触发扩容
	Segment<K,V> s0 =
		new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
						 (HashEntry<K,V>[])new HashEntry[cap]);
	//创建segments数组并初始化并发量的大小ssize
    Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
	安全的将segment0赋值到segments[0]
    UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
	this.segments = ss;
}

通过构造函数我们可以总结一下知识

  • Segment数量默认是16,初始容量默认是16,负载因子默认是0.75,最小Segment是2
  • Segment的数量即为并发级别,且内部保证是2的幂,Segment内部的table大小也保证为2的幂
  • Segment数量一旦确定不会在更改,后续添加元素不会增加Segment的数量,而是增加Segment中链表数组的容量,这样的好处是扩容也不用针对整个ConcurrentHashMap来进行了,而是针对Segment里面的数组
  • 初始化了Segment[0],其他Segment还是null

A)Unsafe类

Java提供的操作内存的类,具体详情可以查看这篇博客:https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html

2.java8

// node数组最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认初始值,必须是2的幕数
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;
// 链表转红黑树阀值,> 8 链表转换为红黑树
static final int TREEIFY_THRESHOLD = 8;
//树转链表阀值,小于等于6
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;
//help resize的最大线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
// 32-16=16,sizeCtl中记录size大小的偏移量
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
// forwarding nodes的hash值
static final int MOVED     = -1;
// 树根节点的hash值
static final int TREEBIN   = -2;
// ReservationNode的hash值
static final int RESERVED  = -3;
// 可用处理器数量
static final int NCPU = Runtime.getRuntime().availableProcessors();
//存放node的数组
transient volatile Node<K,V>[] table;
/*控制标识符,用来控制table的初始化和扩容的操作,不同的值有不同的含义
 *当为负数时:-1代表正在初始化,-N代表有N-1个线程正在 进行扩容
 *当为0时:代表当时的table还没有被初始化
 *当为正数时:表示初始化或者下一次进行扩容的大小
*/
private transient volatile int sizeCtl;
//扩容时新生成的数组,其大小为原数组的两倍。
private transient volatile Node<K,V>[] nextTable;

class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    //... 省略部分代码
}

//默认构造函数
 public ConcurrentHashMap() {
 }

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


public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
	this.sizeCtl = DEFAULT_CAPACITY;
	putAll(m);
}


public ConcurrentHashMap(int initialCapacity, float loadFactor) {
	this(initialCapacity, loadFactor, 1);
}


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
	long size = (long)(1.0 + (long)initialCapacity / loadFactor);
	int cap = (size >= (long)MAXIMUM_CAPACITY) ?
		MAXIMUM_CAPACITY : tableSizeFor((int)size);
	this.sizeCtl = cap;
}

四、put函数

1.java7

public V put(K key, V value) {
	Segment<K,V> s;
	//value不能为空
	if (value == null)
		throw new NullPointerException();
	//通过hash函数获取关于key的hash值
	int hash = hash(key);
	//计算要插入的Segment数组的下标,位运算提高计算速度,由于此处使用位运算,所以得保证是2的幂可以减少hash冲突   
	int j = (hash >>> segmentShift) & segmentMask;
	//如果要插入的Segment为初始化,调用ensureSeggment函数进行初始化(初始化concurrentHashMap时只初始化了第一个Segment[0])
	if ((s = (Segment<K,V>)UNSAFE.getObject         
		 (segments, (j << SSHIFT) + SBASE)) == null) ent
		//初始化Segment[j]
        s = ensureSegment(j);
	//调用Segment的put函数,插入新值到 槽 s 中
	return s.put(key, hash, value, false);
}

//Segment的put函数
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    //先尝试对segment加锁,如果直接加锁成功,那么node=null;如果加锁失败,则会调用scanAndLockForPut方法去获取锁,
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    //到此处肯定已经获取到锁了
    V oldValue;
    try {
        //这里是一个优化点,由于table自身是被volatile修饰的,然而put这一块代码本身是加锁了的,所以同一时间内只会有一个线程操作这部分内容,
        //所以不再需要对这一块内的变量做任何volatile修饰,因为变量加了volatile修饰后,变量无法进行编译优化等,会对性能有一定的影响
        //故将table赋值给put方法中的一个局部变量,从而使得能够减少volatile带来的不必要消耗。
        HashEntry<K,V>[] tab = table;
        //计算元素插入的位置
        int index = (tab.length - 1) & hash;
        //定位到第index个HashEntry,也就是得到该链表的头结点
        HashEntry<K,V> first = entryAt(tab, index);
        //遍历first为头结点的链表
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                //e不为空,说明当前键值对需要存储的位置有hash冲突,直接遍历当前链表,如果链表中找到一个节点对应的key相同,
                //依据onlyIfAbsent来判断是否覆盖已有的value值。
                K k;
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    //进入这个条件内说明需要put的<k,y>对应的key节点已经存在,直接判断是否更新并最后break退出循环。
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                //遍历下一个节点。
                e = e.next;
            }
            //已经遍历到链表尾部
            else {
                if (node != null) 
                    node.setNext(first);
                else
                    node = new HashEntry<K,V>(hash, key, value, first);
                //当前Segment中元素个数+1
                int c = count + 1; 
                //判断是否需要进行扩容
                if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                    //扩容
                    rehash(node);
                else
                    // 没有达到阈值,将 node 放到数组 tab 的 index 位置,将新的节点设置成原链表的表头
                    setEntryAt(tab, index, node);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}

至此,put方法主流程已全部走完,下面详细介绍下ensureSegment函数,scanAndLockForPut函数,rehash函数

A)ensureSegment函数

在ConcurrentHashMap初始化时,只初始化了Segment[0],其他的Segment数组都是null,调用ensureSegment去初始化Segment[j],但是在该方法中需要注意可能有多个线程同时调用初始化同一个槽 segment[j]。这个方法核心思想就是利用自旋CAS来创建对应Segment

private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; 
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        // 这里看到为什么之前要初始化 segment[0] 了,
        // 使用当前 segment[0] 处的数组长度和负载因子来初始化 segment[k]
        Segment<K,V> proto = ss[0];
        int cap = proto.table.length;
        float lf = proto.loadFactor;
        int threshold = (int)(cap * lf);

        // 初始化 segment[k] 内部的数组
        HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
         // 再次检查一遍该槽是否被其他线程初始化了。
        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
            == null) {
            Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
            // //这里通过自旋的CAS方式对segments数组中偏移量为u位置设置值为s,这是一种不加锁的方式,当前线程成功设值或其他线程成功设值后,退出
            while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                   == null) {
                if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                    break;
            }
        }
    }
    return seg;
}

B)scanAndLockForPut函数

获取锁

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    //根据this(Segment)和hash确定table中的索引,拿到key所在链表的第一个节点
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; 
   //获取锁失败时一直循环,除非tryLock成功或者达到自旋次数,直接Lock,退出
    while (!tryLock()) {
        HashEntry<K,V> f; 
        //没有找到key相同的节点或者没有遍历完该链表
        if (retries < 0) {
            //如果当前索引链表为空,或者循环到链表的最后
            if (e == null) { 
                if (node == null) 
                    node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            // 遍历过程发现链表中找到了我们需要的key的坑位    
            else if (key.equals(e.key))  
                retries = 0;
            //当前位置对应的key不是我们需要的,遍历下一个
            else   
                e = e.next;
        }
         //无限制的自旋是不利的,判断是否大于最大自旋次数,超过这个次数则进入阻塞状态等待对方释放锁并获取锁。
        else if (++retries > MAX_SCAN_RETRIES) {
            lock();
            break;
        }
        //因为插入元素是头插法,所以这块是为了判断首个预期值是否等于现在的值
        //如果不等于,则证明其他线程已经插入了元素,这时就需要重新进行遍历了
        else if ((retries & 1) == 0 &&
                 (f = entryForHash(this, hash)) != first) {
            e = first = f; 
            retries = -1;
        }
    }
    return node;
}

C)rehash函数

查看该函数的代码之前先了解一些知识点

  • 对Segment中的table进行的扩容,不是对ConcurrentHashMap的segments
  • 扩容是2倍扩容
  • 扩容后的元素存储位置有两种情况,一是任然处于原位置idex处,二是处于idex+扩容容量大小的位置。

方法思想:扩容函数对数组进行扩容,由于扩容过程需要将老的链表中的节点适用到新数组中,所以为了优化效率,可以对已有链表进行遍历,  对于老的oldTable中的每个HashEntry,从头结点开始遍历,找到第一个后续所有节点在新table中index保持不变的节点node,那么这个节点node后续的节点也是在新table中的idex也保持不变。 假设这个节点新的index为newIndex,那么直接newTable[newIndex]=node,这样即可以直接将这个节点以及它后续的链表中内容全部直接复用copy到newTable中 。然后对于每个HashEntry的头结点到在新table中index保持不变的节点node期间的所有节点,在遍历结算hash值,重新插入该存放的位置

private void rehash(HashEntry<K,V> node) {
	HashEntry<K,V>[] oldTable = table;
	int oldCapacity = oldTable.length;
	//2倍扩容
	int newCapacity = oldCapacity << 1;
	//计算阈值
	threshold = (int)(newCapacity * loadFactor);
	//创建新的table
	HashEntry<K,V>[] newTable =
		(HashEntry<K,V>[]) new HashEntry[newCapacity];
	//该值用于后面的hash运算与,保证运算后的值落到数组的范围内
	int sizeMask = newCapacity - 1;
	//遍历旧表中的每一个头结点
	for (int i = 0; i < oldCapacity ; i++) {
		HashEntry<K,V> e = oldTable[i];
		//判断该位置头结点是否为空,为空就没必要遍历该条链表
		if (e != null) {
			//第一个数据节点
			HashEntry<K,V> next = e.next;
			//e节点的新的位置,保证idx落到数组的范围内,这块其实相当于重新hash,sizeMask是newCapacity得来的
			int idx = e.hash & sizeMask;
			//如果只有一个数据节点,那么直接挪到相应的位置
			if (next == null)   
				newTable[idx] = e;
			else {
				HashEntry<K,V> lastRun = e;
				//记录lastRun节点的位置
				int lastIdx = idx;
				//遍历获取第一个后续节点新的index不变的节点
                for (HashEntry<K,V> last = next;
					 last != null;
					 last = last.next) {
					int k = last.hash & sizeMask;
					//如果k=lastIdx,就不更新lastIdx和lastRun,找到可重用部分的链表头部
					if (k != lastIdx) {
						lastIdx = k;
						lastRun = last;
					}
				}
				//将lastRun后面所有链表一同挪过去
				newTable[lastIdx] = lastRun;
				//从头到lastRun(可重用链表头部)一个一个的重新hash,放入该放的位置,方式为头插
				for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
					V v = p.value;
					int h = p.hash;
					int k = h & sizeMask;
					HashEntry<K,V> n = newTable[k];
					newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
				}
			}
		}
	}
	//把新的节点重哈希,添加到新table中
	int nodeIndex = node.hash & sizeMask; // add the new node
	node.setNext(newTable[nodeIndex]);
	newTable[nodeIndex] = node;
	table = newTable;
}

2.java8

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();
    //两次hash,减少hash冲突,可以均匀分布
    int hash = spread(key.hashCode()); 
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //这里就是上面构造方法没有进行初始化,在这里进行判断,为null就调用initTable进行初始化,属于懒汉模式初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //如果i位置没有数据,就直接无锁插入
        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;
            //如果以上条件都不满足,那就要进行加锁操作,也就是存在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;
            }
        }
    }
    //统计size,并且检查是否需要扩容
    addCount(1L, binCount);
    return null;
}

上述代码put的主流程已经走完

a)initTable()函数

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    //空的table才能进入初始化操作
    while ((tab = table) == null || tab.length == 0) {
        //sizeCtl<0表示其他线程已经在初始化了或者扩容了,挂起当前线程
        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 = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

b)helpTransfer帮助扩容函数

**
 *帮助从旧的table的元素复制到新的table中
 */
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    //新的table nextTba已经存在前提下才能帮助扩容
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) { 
        int rs = resizeStamp(tab.length);
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                //调用扩容方法
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

C)transfer()扩容方法

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        // 每核处理的量小于16,则强制赋值16
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
            stride = MIN_TRANSFER_STRIDE; // subdivide range
        if (nextTab == null) {            // initiating
            try {
                @SuppressWarnings("unchecked")
                 //构建一个nextTable对象,其容量为原来容量的两倍
                Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];       
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        // 连接点指针,用于标志位(fwd的hash值为-1,fwd.nextTable=nextTab)
        ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
        // 当advance == true时,表明该节点已经处理过了
        boolean advance = true;
        boolean finishing = false; // to ensure sweep before committing nextTab
        for (int i = 0, bound = 0;;) {
            Node<K,V> f; int fh;
            // 控制 --i ,遍历原hash表中的节点
            while (advance) {
                int nextIndex, nextBound;
                if (--i >= bound || finishing)
                    advance = false;
                else if ((nextIndex = transferIndex) <= 0) {
                    i = -1;
                    advance = false;
                }
                // 用CAS计算得到的transferIndex
                else if (U.compareAndSwapInt
                        (this, TRANSFERINDEX, nextIndex,
                                nextBound = (nextIndex > stride ?
                                        nextIndex - stride : 0))) {
                    bound = nextBound;
                    i = nextIndex - 1;
                    advance = false;
                }
            }
            if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                // 已经完成所有节点复制了
                if (finishing) {
                    nextTable = null;
                    // table 指向nextTable
                    table = nextTab;   
                     // sizeCtl阈值为原来的1.5倍     
                    sizeCtl = (n << 1) - (n >>> 1);    
                    return;     
                }
                // CAS 更扩容阈值,在这里面sizectl值减一,说明新加入一个线程参与到扩容操作
                if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                        return;
                    finishing = advance = true;
                    i = n; // recheck before commit
                }
            }
            // 遍历的节点为null,则放入到ForwardingNode 指针节点
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            // f.hash == -1 表示遍历到了ForwardingNode节点,意味着该节点已经处理过了
            // 这里是控制并发扩容的核心
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                // 节点加锁
                synchronized (f) {
                    // 节点复制工作
                    if (tabAt(tab, i) == f) {
                        Node<K,V> ln, hn;
                        // fh >= 0 ,表示为链表节点
                        if (fh >= 0) {
                            // 构造两个链表  一个是原链表  另一个是原链表的反序排列
                            int runBit = fh & n;
                            Node<K,V> lastRun = f;
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                int b = p.hash & n;
                                if (b != runBit) {
                                    runBit = b;
                                    lastRun = p;
                                }
                            }
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            }
                            else {
                                hn = lastRun;
                                ln = null;
                            }
                            for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                int ph = p.hash; K pk = p.key; V pv = p.val;
                                if ((ph & n) == 0)
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                else
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                            }
                            // 在nextTable i 位置处插上链表
                            setTabAt(nextTab, i, ln);
                            // 在nextTable i + n 位置处插上链表
                            setTabAt(nextTab, i + n, hn);
                            // 在table i 位置处插上ForwardingNode 表示该节点已经处理过了
                            setTabAt(tab, i, fwd);
                            // advance = true 可以执行--i动作,遍历节点
                            advance = true;
                        }
                        // 如果是TreeBin,则按照红黑树进行处理,处理逻辑与上面一致
                        else if (f instanceof TreeBin) {
                            TreeBin<K,V> t = (TreeBin<K,V>)f;
                            TreeNode<K,V> lo = null, loTail = null;
                            TreeNode<K,V> hi = null, hiTail = null;
                            int lc = 0, hc = 0;
                            for (Node<K,V> e = t.first; e != null; e = e.next) {
                                int h = e.hash;
                                TreeNode<K,V> p = new TreeNode<K,V>
                                        (h, e.key, e.val, null, null);
                                if ((h & n) == 0) {
                                    if ((p.prev = loTail) == null)
                                        lo = p;
                                    else
                                        loTail.next = p;
                                    loTail = p;
                                    ++lc;
                                }
                                else {
                                    if ((p.prev = hiTail) == null)
                                        hi = p;
                                    else
                                        hiTail.next = p;
                                    hiTail = p;
                                    ++hc;
                                }
                            }
                            // 扩容后树节点个数若<=6,将树转链表
                            ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                    (hc != 0) ? new TreeBin<K,V>(lo) : t;
                            hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                    (lc != 0) ? new TreeBin<K,V>(hi) : t;
                            setTabAt(nextTab, i, ln);
                            setTabAt(nextTab, i + n, hn);
                            setTabAt(tab, i, fwd);
                            advance = true;
                        }
                    }
                }
            }
        }
    }

d)addCount()方法

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    //更新baseCount,table的数量,counterCells表示元素个数的变化
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        //如果多个线程都在执行,则CAS失败,执行fullAddCount,全部加入count
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
     //check>=0表示需要进行扩容操作
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n);
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            //当前线程发起库哦哦让操作,nextTable=null
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

看到这里我们已经可以得到Q1,Q2,Q4的答案了

Q1:CurrentHashMap的实现原理

在JDK1.7中ConcurrentHashMap采用了数组+Segment+分段锁的方式实现。Segment,类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表。

结构示意图如下

而在java8中ConcurrentHashMap摒弃了1.7的segment设计,而是在1.8HashMap的基础上实现了线程安全的版本,即也是采用数组+链表+红黑树的形式。

数组可以扩容,链表可以转化为红黑树,并利用CAS + synchronized来保证并发更新的安全

结构示意图可以查看我写的hashmap源码查看的博客

Q2:CurrentHashMap如何实现put数据的

对于1.7而言put实现步骤如下

1.先计算出key值的hash值,然后通过hash值找到数组中对应的segment对象,若当前插入的segment对象未初始化,则初始化
 2.尝试获取锁,失败则自旋保证成功。
 3.获取锁,然后通过计算出的hash值找出对应的entry对象,遍历链表中,查找有没有相同key值对象,有旧值覆盖新值,没有旧值添加到链表中

对于1.8而言,步骤如下

  1. 如果没有初始化就先调用initTable()方法来进行初始化过程
  2. 如果没有hash冲突就直接CAS插入
  3. 如果还在进行扩容操作就先进行扩容
  4. 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入,
  5. 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环(阿里面试官问题,默认的链表大小,超过了这个值就会转换为红黑树);
  6. 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容

Q4:CurrentHashMap的扩容机制是什么

   1.7

    在1.7中扩容的实质是对Segment中的table进行的扩容,也就是hashentry数组的扩容,每次扩容后是前面的容量的2倍,在对数组进行扩容时,原来的元素的存储位置会有两种情况,一个是任然处于新数组中的原来的idex位置,要么就出处于idex+扩容容量的位置。所以在扩容中为了优化效率,可以对已有链表进行遍历,  对于老的oldTable中的每个HashEntry,从头结点开始遍历,找到第一个后续所有节点在新table中index保持不变的节点node,那么这个节点node后续的节点也是在新table中的idex也保持不变。 假设这个节点新的index为newIndex,那么直接newTable[newIndex]=node,这样即可以直接将这个节点以及它后续的链表中内容全部直接复用copy到newTable中 。然后对于每个HashEntry的头结点到在新table中index保持不变的节点node期间的所有节点,在遍历结算hash值,重新插入该存放的位置。

   1.8

   如果新增节点之后,所在的链表的元素个数大于等于8,则会调用treeifyBin把链表转换为红黑树。在转换结构时,若tab的长度小于MIN_TREEIFY_CAPACITY,默认值为64,则会将数组长度扩大到原来的两倍,并触发transfer,重新调整节点位置。(只有当tab.length >= 64, ConcurrentHashMap才会使用红黑树。)
新增节点后,addCount统计tab中的节点个数大于阈值(sizeCtl),会触发transfer,重新调整节点位置。而对于transfer函数而言,当table的元素数量达到容量阈值sizeCtl,需要对table进行扩容:构建一个nextTable,大小为table两倍,把table的数据复制到nextTable中。在扩容过程中,依然支持并发更新操作;也支持并发插入。

五、get函数

1.java7

整个get函数相对来是实现思路不复杂,先找到在哪个Segment数组中,再去寻找具体在哪个table上,整个过程没加锁,因为Sigment中的HashEntry和HashEntry中的value都是由volatile修饰的,volatile保证了内存的可见性。

public V get(Object key) {
	Segment<K,V> s; 
	HashEntry<K,V>[] tab;
	int h = hash(key);
	//先计算在哪个segment数组中
	long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
	if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
		(tab = s.table) != null) {
		//计算在segment数组中的哪个HashEntry上,然后遍历链表
		for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
				 (tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
			 e != null; e = e.next) {
			K k;
         
			//key值和当前节点的key指向同一片地址,或者当前节点的hash等于key的hash并且equals比价后相同则说明是目标节点            
			if ((k = e.key) == key || (e.hash == h && key.equals(k)))
				return e.value;
		}
	}
	return null;
}

2.java8

ConcurrentHashMap的get操作的流程很简单,也很清晰,可以分为三个步骤来描述

  1. 计算hash值,定位到该table索引位置,如果是首节点符合就返回
  2. 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
  3. 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回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()); //计算两次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;
        //既不是首节点也不是ForwardingNode,那就往下遍历
        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.java7

public int size() {
    final Segment<K,V>[] segments = this.segments;
    int size;
    boolean overflow; 
    // 存储本次循环过程中计算得到的modCount的值
    long sum;         
    // 存储上一次遍历过程中计算得到的modCount的和
    long last = 0L;   
    int retries = -1; 
    try {
        for (;;) {
            //超过这个次数则直接强制对所有的segment进行加锁
            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<K,V> seg = segmentAt(segments, j);
                if (seg != null) {
                    sum += seg.modCount;
                    int c = seg.count;
                    if (c < 0 || (size += c) < 0)
                        overflow = true;
                }
            }
            //第一轮循环的时候,这个条件不会满足,理想情况下(计算size的时候,没有其他线程进行增删操作)也只有第二轮才会break
            //第一轮循环的时候,先将各个segment中的元素个数累加起来赋值给last,然后第二轮的循环的时候,进行比较,如果相同直接跳出循环
            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;
}

2.java8

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

七、remove方法

1.java7

​
public V remove(Object key) {
	//计算hash    
    int hash = hash(key.hashCode());
	//计算所处的Segment
    Segment<K,V> s = segmentForHash(hash);
	return s == null ? null : s.remove(key, hash, null);
}

final V remove(Object key, int hash, Object value) {
	//如果尝试获取锁失败,就调用方法确保获取到锁
	if (!tryLock())
		scanAndLock(key, hash);
	V oldValue = null;
	try {
		//拿到table数组,算出位置,通过这两个信息拿到对应链表的第一个结点
		HashEntry<K,V>[] tab = table;
		int index = (tab.length - 1) & hash;
		HashEntry<K,V> e = entryAt(tab, index);
		//定义删除节点的前一个节点
		HashEntry<K,V> pred = null;
		//遍历链表,当没有遍历完的时候
		while (e != null) {
			K k;
			//记录要删除节点的下一个节点
			HashEntry<K,V> next = e.next;
			if ((k = e.key) == key ||
				(e.hash == hash && key.equals(k))) {
				V v = e.value;
				if (value == null || value == v || value.equals(v)) {
					if (pred == null)
						//前一个节点为空,则删除的是第一个节点,直接把下一个节点设为链表头
						setEntryAt(tab, index, next);
					else
						//前一个节点不为空,那么把前一个节点的next设为下一个节点
						pred.setNext(next);
					++modCount;
					--count;
					oldValue = v;
				}
				break;
			}
			pred = e;
			e = next;
		}
	} finally {
		unlock();
	}
	return oldValue;
}

​

2.java8

public V remove(Object key) {
    return replaceNode(key, null, null);
}

final V replaceNode(Object key, V value, Object cv) {
    int hash = spread(key.hashCode());
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0 ||
            (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;  
        else if ((fh = f.hash) == MOVED)
             // 协助扩容 
            tab = helpTransfer(tab, f); 
        else {
            V oldVal = null;
            boolean validated = false;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        validated = true;
                        for (Node<K,V> e = f, pred = null;;) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                V ev = e.val;
                                if (cv == null || cv == ev ||
                                    (ev != null && cv.equals(ev))) {
                                    oldVal = ev;
                                    if (value != null)
                                        e.val = value;
                                     // 非链表头节点,直接删除该节点
                                    else if (pred != null) 
                                        pred.next = e.next;
                                    else  // 更新链表头节点
                                        setTabAt(tab, i, e.next);
                                }
                                break;
                            }
                            pred = e;
                            if ((e = e.next) == null)
                                break;
                        }
                    }
                    else if (f instanceof TreeBin) {
                        validated = true;
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> r, p;
                        if ((r = t.root) != null &&
                            (p = r.findTreeNode(hash, key, null)) != null) {
                            V pv = p.val;
                            if (cv == null || cv == pv ||
                                (pv != null && cv.equals(pv))) {
                                oldVal = pv;
                                if (value != null)
                                    p.val = value;
                                else if (t.removeTreeNode(p))  
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
            }
            if (validated) {
                if (oldVal != null) {
                    if (value == null)
                        addCount(-1L, -1);
                    return oldVal;
                }
                break;
            }
        }
    }
    return null;
}

看到这里我们可以回答Q5,Q6的问题了

Q6:为什么CurrentHashMap的读操作不加锁

    对数据的读写是一个原子操作,那么此时是可以不需要读锁的。如ConcurrentHashMap对数据的读写,写操作是不需要分2次写的(没有中间状态),读操作也是不需要2次读取的。假如一个写操作需要分多次写,必然会有中间状态,如果读不加锁,那么可能就会读到中间状态,那就不对了。

    虽然ConcurrentHashMap的读不需要锁,但是需要保证能读到最新数据,所以必须加volatile。即数组的引用需要加volatile,同时一个Node节点中的val和next属性也必须要加volatile。

Q5:CurrentHashMap如何实现线程安全的

在1.7中总的来概括的说采用了分段锁的机制,当一个线程占用锁时,不会影响到其他的Segment对象。

  • 分开来说的话,对于初始化segment[0]以外的数组,使用了cas操作,保证了线程的安全。
  • 对于添加节点的操作 put 和删除节点的操作 remove 都是要加 segment 上的独占锁的,所以它们之间自然不会有问题。
  • 而对于读取数据get操作是一个读的过程,读的过程并不会修改数据,所以读和读也就是get和get之间是线程安全的。
  • 而对于如果一个线程在put,一个线程在get来说,如果put在get之后,get操作已经遍历到链表的中间,这时候put操作如果操作的是一个已经存在的key值,那么put过后,get是可以立即可见的,为什么呢,可以看看Segment类的Value属性是有volatile修饰的,所以可以保证对其他线程的可见性。如果put操作操作的是一个不存在的key,这个时候,get是读不到数据的,但是同样是线程安全的,只不过需要再次的去读才能读到。如果put在get之前,这样就非常好处理了,因为Segment中的table属性就是volatile修饰的,这时只需要在查找前,获取一下最新状态的的table就可以了,如果修改了就会通过UNSAFE.getObjectVolatile(segments, u)更新table,这样查找的就是正确的结果。
  • 而对于一个线程在get,一个线程在remove。如果remove在get之后,那么 remove 破坏的节点 get 操作已经过去了,那么get也可正常返回元素。如果 remove在get之前,那么还需分两种情况考虑。  1、如果此节点是头结点,那么需要将头结点的 next 设置为数组该位置的元素,table 虽然使用了 volatile 修饰,但是 volatile 并不能提供数组内部操作的可见性保证,所以源码中使用了 UNSAFE 来操作数组,请看方法 setEntryAt。2、如果要删除的节点不是头结点,它会将要删除节点的后继节点接到前驱节点中,这里的并发保证就是 next 属性是 volatile 的。

对于1.8而言总的来说就是抛弃了原来的分段锁,采用了 CAS 和 synchronized 来保证并发的安全

Q7:CurrentHashMap在jdk1.7和1.8中的区别

jdk1.8的实现降低锁的粒度,jdk1.7锁的粒度是基于Segment的,包含多个HashEntry,而jdk1.8锁的粒度就是Node

数据结构:jdk1.7 Segment+HashEntry;jdk1.8 数组+链表+红黑树+CAS+synchronized

Q8:JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock?

  1. 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
  2. JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
  3. 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据

Q9:ConcurrentHashMap能完全替代HashTable吗?

hash table虽然性能上不如ConcurrentHashMap,但并不能完全被取代,两者的迭代器的一致性不同的,hash table的迭代器是强一致性的,而concurrenthashmap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的。
下面是大白话的解释:
- Hashtable的任何操作都会把整个表锁住,是阻塞的。好处是总能获取最实时的更新,比如说线程A调用putAll写入大量数据,期间线程B调用get,线程B就会被阻塞,直到线程A完成putAll,因此线程B肯定能获取到线程A写入的完整数据。坏处是所有调用都要排队,效率较低。
- ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处 是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分 数据。

选择哪一个,是在性能与数据一致性之间权衡。ConcurrentHashMap适用于追求性能的场景,大多数线程都只做insert/delete操作,对读取数据的一致性要求较低。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值