当前位置: 代码迷 >> 综合 >> HashTable、HashMap、ConcurrentHashMap区别详解
  详细解决方案

HashTable、HashMap、ConcurrentHashMap区别详解

热度:35   发布时间:2023-10-29 02:23:47.0

HashTable、HashMap、ConcurrentHashMap是面试的时候面试官经常问道的知识点,今天分别复习一下他们的源码与区别。

一、HashMap

        HashMap是基于hash表实现的,不支持并发操作,数据结构也就是采用数组+链表的形式实现的,其实每一个数组元素也就是一个Entry类型的单链表,下面通过一张图了解HashMap结构。

       HashMap中有以下几个重要的参数:

       capacity:表示数组的初始容量,其值是2^n,默认长度为16.

       loadFactor:负载因子,其默认值为0.75,其值是在哈希表自动扩容前可以达到的多满的一种尺度,当哈希表中的实际条目的个数如果超过负载因子与当前容量的乘积,则要对哈希表进行扩容操作。

      阈值(threshold):是HashMap判断是否需要扩容的一个标准,threshold=容量*loadFactor,如果HashMap中实际条目的个数大于threshold,则进行扩容。

PUT操作

public V put(K key, V value) {// 当插入第一个元素的时候,需要先初始化数组大小if (table == EMPTY_TABLE) {inflateTable(threshold);}// 如果 key 为 nullif (key == null)return putForNullKey(value);//最终会将这个 entry 放到 table[0] 中// 1. 求 key 的 hash 值int hash = hash(key); //key为hashcode值// 2. 找到对应的数组下标int i = indexFor(hash, table.length);// 3. 遍历一下对应下标处的链表,看是否有重复的 key 已经存在,//    如果有,直接覆盖,put 方法返回旧值就结束了for (Entry<K,V> e = table[i]; e != null; e = e.next) {Object k;if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {V oldValue = e.value;e.value = value;e.recordAccess(this);return oldValue;}}modCount++;// 4. 不存在重复的 key,将此 entry 添加到链表中addEntry(hash, key, value, i);return null;

inflateTable()方法,用来做数组的初始化。当第一个元素插入HashMap中,就需要初始化数组的容量,并计算数组的阈值。

?
private void inflateTable(int toSize) {// 保证数组大小一定是 2 的 n 次方。// 比如这样初始化:new HashMap(20),那么处理成初始数组大小是 32int capacity = roundUpToPowerOf2(toSize);// 计算扩容阈值:capacity * loadFactorthreshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);// 算是初始化数组吧table = new Entry[capacity];initHashSeedAsNeeded(capacity); //ignore
}?

indexFor(),是通过hash值与数组长度进行取模运算获得到具体数组的位置

static int indexFor(int hash, int length) {// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";return hash & (length-1);
}

addEntry()添加Entry元素到单链表中,将元素放置到链表的表头

void addEntry(int hash, K key, V value, int bucketIndex) {// 如果当前 HashMap 大小已经达到了阈值,并且新值要插入的数组位置已经有元素了,那么要扩容if ((size >= threshold) && (null != table[bucketIndex])) {// 扩容,后面会介绍一下resize(2 * table.length);// 扩容以后,重新计算 hash 值hash = (null != key) ? hash(key) : 0;// 重新计算扩容后的新的下标bucketIndex = indexFor(hash, table.length);}// 往下看createEntry(hash, key, value, bucketIndex);
}
// 这个很简单,其实就是将新值放到链表的表头,然后 size++
void createEntry(int hash, K key, V value, int bucketIndex) {Entry<K,V> e = table[bucketIndex];table[bucketIndex] = new Entry<>(hash, key, value, e);size++;
}

resize()数组的扩容,当前的size达到阈值后,将数组的大小扩展为原数组的大小的两倍,将新数组替换为原数组。

void resize(int newCapacity) {Entry[] oldTable = table;int oldCapacity = oldTable.length;if (oldCapacity == MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return;}// 新的数组Entry[] newTable = new Entry[newCapacity];// 将原来数组中的值迁移到新的更大的数组中transfer(newTable, initHashSeedAsNeeded(newCapacity));table = newTable;threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

GET过程

主要是以下3步:

1、根据key计算出hash值。

2、根据hash值找到对应数组的下标:hash&(table.length-1).

3、遍历对应下标下数组的位置的链表,直到找到相等的key.

public V get(Object key) {// 之前说过,key 为 null 的话,会被放到 table[0],所以只要遍历下 table[0] 处的链表就可以了if (key == null)return getForNullKey();// Entry<K,V> entry = getEntry(key);return null == entry ? null : entry.getValue();
}final Entry<K,V> getEntry(Object key) {if (size == 0) {return null;}int hash = (key == null) ? 0 : hash(key);// 确定数组下标,然后从头开始遍历链表,直到找到为止for (Entry<K,V> e = table[indexFor(hash, table.length)];e != null;e = e.next) {Object k;if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;}return null;
}

Java8中对HashMap的改进

        我们知道,java7中HashMap的数据结构是数组+链表,但是这种数据结构会有一种缺点,就是如果单链表数据过长,HashMap的查找效率就会下降,因为链表的查找效率为O(n).为了降低这部分的开销,在 Java8 中,当链表中的元素超过了 8 个以后,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。

PUT操作过程

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}// 第三个参数 onlyIfAbsent 如果是 true,那么只有在不存在该 key 时才会进行 put 操作
// 第四个参数 evict 我们这里不关心
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;// 第一次 put 值的时候,会触发下面的 resize(),类似 java7 的第一次 put 也要初始化数组长度// 第一次 resize 和后续的扩容有些不一样,因为这次是数组从 null 初始化到默认的 16 或自定义的初始容量if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;// 找到具体的数组下标,如果此位置没有值,那么直接初始化一下 Node 并放置在这个位置就可以了if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {// 数组该位置有数据Node<K,V> e; K k;// 首先,判断该位置的第一个数据和我们要插入的数据,key 是不是"相等",如果是,取出这个节点if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;// 如果该节点是代表红黑树的节点,调用红黑树的插值方法,本文不展开说红黑树else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {// 到这里,说明数组该位置上是一个链表for (int binCount = 0; ; ++binCount) {// 插入到链表的最后面(Java7 是插入到链表的最前面)if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);// TREEIFY_THRESHOLD 为 8,所以,如果新插入的值是链表中的第 9 个// 会触发下面的 treeifyBin,也就是将链表转换为红黑树if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}// 如果在该链表中找到了"相等"的 key(== 或 equals)if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))// 此时 break,那么 e 为链表中[与要插入的新值的 key "相等"]的 nodebreak;p = e;}}// e!=null 说明存在旧值的key与要插入的key"相等"// 对于我们分析的put操作,下面这个 if 其实就是进行 "值覆盖",然后返回旧值if (e != null) {V oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;// 如果 HashMap 由于新插入这个值导致 size 已经超过了阈值,需要进行扩容,这里和java7不一样,java7中是先扩容,再插值;而这里是先插入,再扩容。if (++size > threshold)resize();afterNodeInsertion(evict);return null;
}

数组扩容

resize()用于数组扩容,扩容之后变成,长度变为原数组的2倍,并进行数据迁移。

final Node<K,V>[] resize() {Node<K,V>[] oldTab = table;int oldCap = (oldTab == null) ? 0 : oldTab.length;int oldThr = threshold;int newCap, newThr = 0;if (oldCap > 0) { // 对应数组扩容if (oldCap >= MAXIMUM_CAPACITY) {threshold = Integer.MAX_VALUE;return oldTab;}// 将数组大小扩大一倍else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&oldCap >= DEFAULT_INITIAL_CAPACITY)// 将阈值扩大一倍newThr = oldThr << 1; // double threshold}else if (oldThr > 0) // 对应使用 new HashMap(int initialCapacity) 初始化后,第一次 put 的时候newCap = oldThr;else {// 对应使用 new HashMap() 初始化后,第一次 put 的时候newCap = DEFAULT_INITIAL_CAPACITY;newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);}if (newThr == 0) {float ft = (float)newCap * loadFactor;newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);}threshold = newThr;// 用新的数组大小初始化新的数组Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];table = newTab; // 如果是初始化数组,到这里就结束了,返回 newTab 即可if (oldTab != null) {// 开始遍历原数组,进行数据迁移。for (int j = 0; j < oldCap; ++j) {Node<K,V> e;if ((e = oldTab[j]) != null) {oldTab[j] = null;// 如果该数组位置上只有单个元素,那就简单了,简单迁移这个元素就可以了if (e.next == null)newTab[e.hash & (newCap - 1)] = e;// 如果是红黑树,具体我们就不展开了else if (e instanceof TreeNode)((TreeNode<K,V>)e).split(this, newTab, j, oldCap);else { // 这块是处理链表的情况,// 需要将此链表拆成两个链表,放到新的数组中,并且保留原来的先后顺序// loHead、loTail 对应一条链表,hiHead、hiTail 对应另一条链表,代码还是比较简单的Node<K,V> loHead = null, loTail = null;Node<K,V> hiHead = null, hiTail = null;Node<K,V> next;do {next = e.next;if ((e.hash & oldCap) == 0) {if (loTail == null)loHead = e;elseloTail.next = e;loTail = e;}else {if (hiTail == null)hiHead = e;elsehiTail.next = e;hiTail = e;}} while ((e = next) != null);if (loTail != null) {loTail.next = null;// 第一条链表newTab[j] = loHead;}if (hiTail != null) {hiTail.next = null;// 第二条链表的新的位置是 j + oldCap,这个很好理解newTab[j + oldCap] = hiHead;}}}}}return newTab;
}

get过程

主要包括以下几步:

1、计算key的hash值,根据hash值找到了对应数据的下标:hash&(length-1)

2、判断数组该位置是否是我们要找的,如果不是,走第三步

3、判断该元素类型是否是TreeNode,如果是,用红黑树的方法获取数据,如果不是,走第四步

4、遍历链表,直到找到(==或equals)相等的key

public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;
}final Node<K,V> getNode(int hash, Object key) {Node<K,V>[] tab; Node<K,V> first, e; int n; K k;if ((tab = table) != null && (n = tab.length) > 0 &&(first = tab[(n - 1) & hash]) != null) {// 判断第一个节点是不是就是需要的if (first.hash == hash && // always check first node((k = first.key) == key || (key != null && key.equals(k))))return first;if ((e = first.next) != null) {// 判断是否是红黑树if (first instanceof TreeNode)return ((TreeNode<K,V>)first).getTreeNode(hash, key);// 链表遍历do {if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))return e;} while ((e = e.next) != null);}}return null;
}

二、ConcurrentHashMap

我们从上面了解到HashMap的情况,但是HashMap不能支持并发操作,是线程不安全的。要想能支持并发操作,ConcurrentHashMap是一种不错的选择。ConcurrentHashMap是由一个个Segment组成的,数组的每一个元素都是Segment,Segment通过继承ReentrantLock来进行加锁的,每次操作需要加锁锁住要操作的segment,这样才能保证线程对ConcurrentHashMap访问线程安全,当前线程对当前segment锁住的情况下,其他线程也能对其他segment进行访问,也就是分段锁的原理,只要保证了每个segment线程安全,也就能保证了全局线程安全。

几个重要的参数:

concurrencyLevel:表示Segment的个数,也就是并发数,默认值为16,创建一个concurrentHashMap也就有16个segments,理论上可以支持16个线程并发操作。这个默认值是初始化的时候为其设置的值,一旦初始化后,这个Segment数组就不能进行扩容。

对于每一个Segment内部来说,每一个segment的结构和HashMap很类似,它包括以下几个参数:

initialCapacity:初始容量,这个值指的是整个ConcurrentHashMap的初始容量,实际分给每个segment.

loadFactor:负载因子,因为segment数组是不可扩容,这个加载因子,是用来对于每一个segment内部扩容来使用的,如果实际的条目(单链表的数目)大于或等于阈值(loadFactor*initialFactor),就进行扩容。

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;// Find power-of-two sizes best matching argumentsint sshift = 0;int ssize = 1;// 计算并行级别 ssize,因为要保持并行级别是 2 的 n 次方while (ssize < concurrencyLevel) {++sshift;ssize <<= 1;}// 我们这里先不要那么烧脑,用默认值,concurrencyLevel 为 16,sshift 为 4// 那么计算出 segmentShift 为 28,segmentMask 为 15,后面会用到这两个值this.segmentShift = 32 - sshift;this.segmentMask = ssize - 1;if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;// initialCapacity 是设置整个 map 初始的大小,// 这里根据 initialCapacity 计算 Segment 数组中每个位置可以分到的大小// 如 initialCapacity 为 64,那么每个 Segment 或称之为"槽"可以分到 4 个int c = initialCapacity / ssize;if (c * ssize < initialCapacity)++c;// 默认 MIN_SEGMENT_TABLE_CAPACITY 是 2,这个值也是有讲究的,因为这样的话,对于具体的槽上,// 插入一个元素不至于扩容,插入第二个的时候才会扩容int cap = MIN_SEGMENT_TABLE_CAPACITY; while (cap < c)cap <<= 1;// 创建 Segment 数组,// 并创建数组的第一个元素 segment[0]Segment<K,V> s0 =new Segment<K,V>(loadFactor, (int)(cap * loadFactor),(HashEntry<K,V>[])new HashEntry[cap]);Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];// 往数组写入 segment[0]UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]this.segments = ss;
}

初始化完成后,得到一个Segment数组。

  1. Segment数组长度为16,不可以扩容。
  2. Segment[i]的默认长度为2,负载因子为0.75,得出初始阈值为1.5,也就是以后插入第一个元素不会触发扩容,当插入第二个袁术时,会触发扩容。
  3. 这里初始化了 segment[0],其他位置还是 null,至于为什么要初始化 segment[0],后面的代码会介绍。
  4. 当前 segmentShift 的值为 32 – 4 = 28,segmentMask 为 16 – 1 = 15,姑且把它们简单翻译为移位数和掩码,这两个值马上就会用到。

put过程

public V put(K key, V value) {Segment<K,V> s;if (value == null)throw new NullPointerException();// 1. 计算 key 的 hash 值int hash = hash(key);// 2. 根据 hash 值找到 Segment 数组中的位置 j//    hash 是 32 位,无符号右移 segmentShift(28) 位,剩下低 4 位,//    然后和 segmentMask(15) 做一次与操作,也就是说 j 是 hash 值的最后 4 位,也就是槽的数组下标int j = (hash >>> segmentShift) & segmentMask;// 刚刚说了,初始化的时候初始化了 segment[0],但是其他位置还是 null,// ensureSegment(j) 对 segment[j] 进行初始化if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck(segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegments = ensureSegment(j);// 3. 插入新值到 槽 s 中return s.put(key, hash, value, false);
}

通过hash值无符号右移28位于segmentMask做&运算获得槽数组的下标j,并且初始化segment[j],然后进行segment内部的put操作。

初始化槽: ensureSegment。

ConcurrentHashMap初始化的时候会初始化第一个槽segment[0],对于其他槽来说,来没有初始化,所以要对segment[j]操作,就要先对segment[j]槽初始化。注意这里可能会有多个线程同时进行初始化同一个槽segment[j],只需要有一个成功就可以了。

private Segment<K,V> ensureSegment(int k) {final Segment<K,V>[] ss = this.segments;long u = (k << SSHIFT) + SBASE; // raw offsetSegment<K,V> seg;if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {// 这里看到为什么之前要初始化 segment[0] 了,// 使用当前 segment[0] 处的数组长度和负载因子来初始化 segment[k]// 为什么要用“当前”,因为 segment[0] 可能早就扩容过了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);// 使用 while 循环,内部用 CAS,当前线程成功设值或其他线程成功设值后,退出while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))== null) {if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))break;}}}return seg;
}

再来看Segment内部的组成(数组+链表),在进行内部操作的时候用到了独占锁,对数据进行保护。

final V put(K key, int hash, V value, boolean onlyIfAbsent) {// 在往该 segment 写入前,需要先获取该 segment 的独占锁//    先看主流程,后面还会具体介绍这部分内容HashEntry<K,V> node = tryLock() ? null :scanAndLockForPut(key, hash, value);V oldValue;try {// 这个是 segment 内部的数组HashEntry<K,V>[] tab = table;// 再利用 hash 值,求应该放置的数组下标int index = (tab.length - 1) & hash;// first 是数组该位置处的链表的表头HashEntry<K,V> first = entryAt(tab, index);// 下面这串 for 循环虽然很长,不过也很好理解,想想该位置没有任何元素和已经存在一个链表这两种情况for (HashEntry<K,V> 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 {// node 到底是不是 null,这个要看获取锁的过程,不过和这里都没有关系。// 如果不为 null,那就直接将它设置为链表表头;如果是null,初始化并设置为链表表头。if (node != null)node.setNext(first);elsenode = new HashEntry<K,V>(hash, key, value, first);int c = count + 1;// 如果超过了该 segment 的阈值,这个 segment 需要扩容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;
}

scanAndLockForPut

因为是线程安全的,所以首先会调用node=tryLock()?null:scanAndLockForPut(key,hash,value),也就是说通过tryLock()快速获得segment的独占锁,如果失败,则进入scanAndLockForPut这个方法获取锁。

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {HashEntry<K,V> first = entryForHash(this, hash);HashEntry<K,V> e = first;HashEntry<K,V> node = null;int retries = -1; // negative while locating node// 循环获取锁while (!tryLock()) {HashEntry<K,V> f; // to recheck first belowif (retries < 0) {if (e == null) {if (node == null) // speculatively create node// 进到这里说明数组该位置的链表是空的,没有任何元素// 当然,进到这里的另一个原因是 tryLock() 失败,所以该槽存在并发,不一定是该位置node = new HashEntry<K,V>(hash, key, value, null);retries = 0;}else if (key.equals(e.key))retries = 0;else// 顺着链表往下走e = e.next;}// 重试次数如果超过 MAX_SCAN_RETRIES(单核1多核64),那么不抢了,进入到阻塞队列等待锁//    lock() 是阻塞方法,直到获取锁后返回else if (++retries > MAX_SCAN_RETRIES) {lock();break;}else if ((retries & 1) == 0 &&// 这个时候是有大问题了,那就是有新的元素进到了链表,成为了新的表头//     所以这边的策略是,相当于重新走一遍这个 scanAndLockForPut 方法(f = entryForHash(this, hash)) != first) {e = first = f; // re-traverse if entry changedretries = -1;}}return node;
}

这个方法有两个出口,一个是 tryLock() 成功了,循环终止,另一个就是重试次数超过了 MAX_SCAN_RETRIES,进到 lock() 方法,此方法会阻塞等待,直到成功拿到独占锁。

这个方法就是看似复杂,但是其实就是做了一件事,那就是获取该 segment 的独占锁,如果需要的话顺便实例化了一下 node。

get过程

  1. 计算hash值,找到对应的segment数组对应的具体的位置。
  2. 然后,在具体的槽中根据hash定位具体的HashEntry
  3. 然后,遍历单链表,直到找到要找到即可
public V get(Object key) {Segment<K,V> s; // manually integrate access methods to reduce overheadHashEntry<K,V>[] tab;// 1. hash 值int h = hash(key);long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;// 2. 根据 hash 找到对应的 segmentif ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&(tab = s.table) != null) {// 3. 找到segment 内部数组相应位置的链表,遍历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;if ((k = e.key) == key || (e.hash == h && key.equals(k)))return e.value;}}return null;
}

并发问题分析

put过程是加锁的,get过程是不加锁的。添加与删除节点操作都是要加segment上独占锁的,所以他们没有任何并发问题。需要考虑的就是get的时候在同一个segment中发生了put或者remove操作。

    put操作的线程的安全性

  • 初始化槽,使用了CAS来初始化Segment中的数组。
  • 添加节点到链表的操作是操作到表头的,所以,如果这个时候get操作在链表遍历的过程已经到了中间,是不影响的。当然两一个问题就是get操作在put操作之后,需要保证刚刚插入表头的节点被读取,这个依赖于setEntry方法中使用的UNSAFE.putOrderedObject。
  • 扩容。扩容是新创建了数组,然后进行迁移数据,最后面将 newTable 设置给属性 table。所以,如果 get 操作此时也在进行,那么也没关系,如果 get 先行,那么就是在旧的 table 上做查询操作;而 put 先行,那么 put 操作的可见性保证就是 table 使用了 volatile 关键字。

HashTable、HashMap、ConcurrentHashMap的区别

  1. HashTable与ConcurrentHashMap都是线程安全,支持多线程操作,HashMap线程不安全,不能支持多线程操作。
  2. HashTable与ConcurrentHashMap虽然都是支持多线程操作,但是他们实现线程安全的方式不同,HashTable是用Synchronize锁住整张hash表实现同步的,而ConcurrentHashMap采用分段锁技术,仅仅锁住了一个桶(Segment),理论上能够支持多个线程并行操作。很显然,ConcurrentHashMap效率要比HashTable高的多。

 

  相关解决方案