HashMap,HashTable,ConcurrentHashMap面试总结!!!

原文:https://www.cnblogs.com/hexinwei1/p/10000779.html

一、小总结

1、HashMap 、HashTable、 ConcurrentHashMap

HashMap:线程不安全

HashTable:线程安全,每个方法都加了 synchronized 修饰。类似 Collections.synchronizedMap(hashMap)

对读写加锁,独占式,一个线程在读时其他线程必须等待,吞吐量较低,性能较为低下。

ConcurrentHashMap:利用CAS+Synchronized来保证并发的安全性。数据结构同HashMap。

2、ConcurrentHashMap如何实现线程安全?

(1)get()方法使用tabAt(Node<k, v="">[], int)方法

调用Unsafe的native方法 getObjectVolatile(Object obj, long offset);
// 获取obj对象中offset偏移地址对应的object型field的值,支持volatile load语义,即:让缓存中的数据失效,重新从主内存加载数据

(2)put()方法

  ①需要获取数组上的Node时同样使用tabAt()方法

  ②设置数组上Node是使用casTabAt() 方法,casTabAt()调用Unsafe的native方法compareAndSwapObject(),CAS操作

  ③哈希冲突之后,需要操作改hash值对应的链表/红黑树,此时synchronized(该链表第一个Node)保证线程安全的基础上,减小了锁的粒度。

3、线程安全的容器只能保证自身的数据不被破坏,但无法保证业务的行为是否正确。
 1 public static void demo1() {
 2         final Map<String, Integer> count = new ConcurrentHashMap<>();
 3         final CountDownLatch endLatch = new CountDownLatch(2);
 4         Runnable task = new Runnable() {
 5             @Override
 6             public void run() {
 7                 for (int i = 0; i < 5; i++) {
 8                     Integer value = count.get("a");
 9                     if (null == value) {
10                         count.put("a", 1);
11                     } else {
12                         count.put("a", value + 1);
13                     }
14                 }
15                 endLatch.countDown();
16             }
17         };
18         new Thread(task).start();
19         new Thread(task).start();
20 
21         try {
22             endLatch.await();
23             System.out.println(count);
24         } catch (Exception e) {
25             e.printStackTrace();
26         }
27     }

demo1是两个线程操作ConcurrentHashMap,意图将value变为10。但是,因为多个线程用相同的key调用时,很可能会覆盖相互的结果,造成记录的次数比实际出现的次数少。

当然可以用锁解决这个问题,但是也可以使用ConcurrentMap定义的方法:

V putIfAbsent(K key, V value)
   如果key对应的value不存在,则put进去,返回null。否则不put,返回已存在的value。

boolean remove(Object key, Object value)
   如果key对应的值是value,则移除K-V,返回true。否则不移除,返回false。

boolean replace(K key, V oldValue, V newValue)
   如果key对应的当前值是oldValue,则替换为newValue,返回true。否则不替换,返回false。

修改:

 1 public static void demo1() {
 2     final Map<String, Integer> count = new ConcurrentHashMap<>();
 3     final CountDownLatch endLatch = new CountDownLatch(2);
 4     Runnable task = new Runnable() {
 5         @Override
 6         public void run() {
 7             Integer oldValue, newValue;
 8             for (int i = 0; i < 5; i++) {
 9                 while (true) {
10                     oldValue = count.get("a");
11                     if (null == oldValue) {
12                         newValue = 1;
13                         if (count.putIfAbsent("a", newValue) == null) {
14                             break;
15                         }
16                     } else {
17                         newValue = oldValue + 1;
18                         if (count.replace("a", oldValue, newValue)) {
19                             break;
20                         }
21                     }
22                 }
23             }
24             endLatch.countDown();
25         }
26     };
27     new Thread(task).start();
28     new Thread(task).start();
29 
30     try {
31         endLatch.await();
32         System.out.println(count);
33     } catch (Exception e) {
34         e.printStackTrace();
35     }
36 }

由于ConcurrentMap中不能保存value为null的值,所以需要处理不存在和已存在两种情况,不过可以使用AtomicInteger来替代。

 1 public static void demo1() {
 2     final Map<String, AtomicInteger> count = new ConcurrentHashMap<>();
 3     final CountDownLatch endLatch = new CountDownLatch(2);
 4     Runnable task = new Runnable() {
 5         @Override
 6         public void run() {
 7             AtomicInteger oldValue;
 8             for (int i = 0; i < 5; i++) {
 9                 oldValue = count.get("a");
10                 if (null == oldValue) {
11                     AtomicInteger zeroValue = new AtomicInteger(0);
12                     oldValue = count.putIfAbsent("a", zeroValue);
13                     if (null == oldValue) {
14                         oldValue = zeroValue;
15                     }
16                 }
17                 oldValue.incrementAndGet();
18             }
19             endLatch.countDown();
20         }
21     };
22     new Thread(task).start();
23     new Thread(task).start();
24 
25     try {
26         endLatch.await();
27         System.out.println(count);
28     } catch (Exception e) {
29         e.printStackTrace();
30     }
31 }

二、属性

 1 // 最大容量:2^30=1073741824
 2 private static final int MAXIMUM_CAPACITY = 1 << 30;
 3 
 4 // 默认初始值,必须是2的幕数
 5 private static final int DEFAULT_CAPACITY = 16;
 6 
 7 //
 8 static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
 9 
10 //
11 private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
12 
13 //
14 private static final float LOAD_FACTOR = 0.75f;
15 
16 // 链表转红黑树阀值,> 8 链表转换为红黑树
17 static final int TREEIFY_THRESHOLD = 8;
18 
19 //树转链表阀值,小于等于6(tranfer时,lc、hc=0两个计数器分别++记录原bin、新binTreeNode数量,<=UNTREEIFY_THRESHOLD 则untreeify(lo))
20 static final int UNTREEIFY_THRESHOLD = 6;
21 
22 //
23 static final int MIN_TREEIFY_CAPACITY = 64;
24 
25 //
26 private static final int MIN_TRANSFER_STRIDE = 16;
27 
28 //
29 private static int RESIZE_STAMP_BITS = 16;
30 
31 // 2^15-1,help resize的最大线程数
32 private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
33 
34 // 32-16=16,sizeCtl中记录size大小的偏移量
35 private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
36 
37 // forwarding nodes的hash值
38 static final int MOVED     = -1;
39 
40 // 树根节点的hash值
41 static final int TREEBIN   = -2;
42 
43 // ReservationNode的hash值
44 static final int RESERVED  = -3;
45 
46 // 可用处理器数量
47 static final int NCPU = Runtime.getRuntime().availableProcessors();

几个很重要的概念:

(1)table:用来存放Node节点数据的,默认为null,默认大小为16的数组,每次扩容时大小总是2的幂次方;

(2)nextTable:扩容时新生成的数据,数组为table的两倍;

(3)Node:节点,保存key-value的数据结构;

(4)ForwardingNode:一个特殊的Node节点,hash值为-1,其中存储nextTable的引用。只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被移动

(5)sizeCtl:控制标识符,用来控制table初始化和扩容操作的,在不同的地方有不同的用途,其值也不同,所代表的含义也不同

  1. 负数代表正在进行初始化或扩容操作

  2. -1代表正在初始化

  3. -N 表示有N-1个线程正在进行扩容操作

  4. 正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小

三、构造

 1 public ConcurrentHashMap() {
 2     }
 3 
 4     public ConcurrentHashMap(int initialCapacity) {
 5         if (initialCapacity < 0)
 6             throw new IllegalArgumentException();
 7         int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
 8                    MAXIMUM_CAPACITY :
 9                    tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
10         this.sizeCtl = cap;
11     }
12 
13     public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
14         this.sizeCtl = DEFAULT_CAPACITY;
15         putAll(m);
16     }
17 
18     public ConcurrentHashMap(int initialCapacity, float loadFactor) {
19         this(initialCapacity, loadFactor, 1);
20     }
21 
22     public ConcurrentHashMap(int initialCapacity,
23                              float loadFactor, int concurrencyLevel) {
24         if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
25             throw new IllegalArgumentException();
26         if (initialCapacity < concurrencyLevel)   // Use at least as many bins
27             initialCapacity = concurrencyLevel;   // as estimated threads
28         long size = (long)(1.0 + (long)initialCapacity / loadFactor);
29         int cap = (size >= (long)MAXIMUM_CAPACITY) ?
30             MAXIMUM_CAPACITY : tableSizeFor((int)size);
31         this.sizeCtl = cap;
32     }

四、put()

 1 public V put(K key, V value) {
 2     return putVal(key, value, false);
 3 }
 4 
 5 final V putVal(K key, V value, boolean onlyIfAbsent) {
 6     if (key == null || value == null) throw new NullPointerException();
 7     // 得到 hash 值
 8     int hash = spread(key.hashCode());
 9     // 用于记录相应链表的长度
10     int binCount = 0;
11     for (Node<K,V>[] tab = table;;) {
12         Node<K,V> f; int n, i, fh;
13         // 如果数组"空",进行数组初始化
14         if (tab == null || (n = tab.length) == 0)
15             // 初始化数组,后面会详细介绍
16             tab = initTable();
17 
18         // 找该 hash 值对应的数组下标,得到第一个节点 f
19         else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
20             // 如果数组该位置为空,
21             //    用一次 CAS 操作将这个新值放入其中即可,这个 put 操作差不多就结束了,可以拉到最后面了
22             //          如果 CAS 失败,那就是有并发操作,进到下一个循环就好了
23             if (casTabAt(tab, i, null,
24                          new Node<K,V>(hash, key, value, null)))
25                 break;                   // no lock when adding to empty bin
26         }
27         // hash 居然可以等于 MOVED,这个需要到后面才能看明白,不过从名字上也能猜到,肯定是因为在扩容
28         else if ((fh = f.hash) == MOVED)
29             // 帮助数据迁移,这个等到看完数据迁移部分的介绍后,再理解这个就很简单了
30             tab = helpTransfer(tab, f);
31 
32         else { // 到这里就是说,f 是该位置的头结点,而且不为空
33 
34             V oldVal = null;
35             // 获取数组该位置的头结点的监视器锁
36             synchronized (f) {
37                 if (tabAt(tab, i) == f) {
38                     if (fh >= 0) { // 头结点的 hash 值大于 0,说明是链表
39                         // 用于累加,记录链表的长度
40                         binCount = 1;
41                         // 遍历链表
42                         for (Node<K,V> e = f;; ++binCount) {
43                             K ek;
44                             // 如果发现了"相等"的 key,判断是否要进行值覆盖,然后也就可以 break 了
45                             if (e.hash == hash &&
46                                 ((ek = e.key) == key ||
47                                  (ek != null && key.equals(ek)))) {
48                                 oldVal = e.val;
49                                 if (!onlyIfAbsent)
50                                     e.val = value;
51                                 break;
52                             }
53                             // 到了链表的最末端,将这个新值放到链表的最后面
54                             Node<K,V> pred = e;
55                             if ((e = e.next) == null) {
56                                 pred.next = new Node<K,V>(hash, key,
57                                                           value, null);
58                                 break;
59                             }
60                         }
61                     }
62                     else if (f instanceof TreeBin) { // 红黑树
63                         Node<K,V> p;
64                         binCount = 2;
65                         // 调用红黑树的插值方法插入新节点
66                         if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
67                                                        value)) != null) {
68                             oldVal = p.val;
69                             if (!onlyIfAbsent)
70                                 p.val = value;
71                         }
72                     }
73                 }
74             }
75 
76             if (binCount != 0) {
77                 // 判断是否要将链表转换为红黑树,临界值和 HashMap 一样,也是 8
78                 if (binCount >= TREEIFY_THRESHOLD)
79                     // 这个方法和 HashMap 中稍微有一点点不同,那就是它不是一定会进行红黑树转换,
80                     // 如果当前数组的长度小于 64,那么会选择进行数组扩容,而不是转换为红黑树
81                     //    具体源码我们就不看了,扩容部分后面说
82                     treeifyBin(tab, i);
83                 if (oldVal != null)
84                     return oldVal;
85                 break;
86             }
87         }
88     }
89     // 
90     addCount(1L, binCount);
91     return null;
92 }

按照上面的源码,我们可以确定put整个流程如下:

  • 判空;ConcurrentHashMap的key、value都不允许为null

  • 计算hash。利用方法计算hash值。

  • 遍历table,进行节点插入操作,过程如下:

    • 如果table为空,则表示ConcurrentHashMap还没有初始化,则进行初始化操作:initTable()

    • 根据hash值获取节点的位置i,若该位置为空,则直接插入,这个过程是不需要加锁的。计算f位置:i=(n – 1) & hash

    • 如果检测到fh = f.hash == -1,则f是ForwardingNode节点,表示有其他线程正在进行扩容操作,则帮助线程一起进行扩容操作

    • 如果f.hash >= 0 表示是链表结构,则遍历链表,如果存在当前key节点则替换value,否则插入到链表尾部。如果f是TreeBin类型节点,则按照红黑树的方法更新或者增加节点

    • 若链表长度 > TREEIFY_THRESHOLD(默认是8),则将链表转换为红黑树结构

  • 调用addCount方法,ConcurrentHashMap的size + 1

这里整个put操作已经完成。

五、get()

 1 public V get(Object key) {
 2         Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
 3         // 计算hash
 4         int h = spread(key.hashCode());
 5         if ((tab = table) != null && (n = tab.length) > 0 &&
 6                 (e = tabAt(tab, (n - 1) & h)) != null) {
 7             // 搜索到的节点key与传入的key相同且不为null,直接返回这个节点
 8             if ((eh = e.hash) == h) {
 9                 if ((ek = e.key) == key || (ek != null && key.equals(ek)))
10                     return e.val;
11             }
12             //
13             else if (eh < 0)
14                 return (p = e.find(h, key)) != null ? p.val : null;
15             // 链表,遍历
16             while ((e = e.next) != null) {
17                 if (e.hash == h &&
18                         ((ek = e.key) == key || (ek != null && key.equals(ek))))
19                     return e.val;
20             }
21         }
22         return null;
23     }

get操作:
   - 计算hash值
   - 判断table是否为空,如果为空,直接返回null
   - 根据hash值获取table中的Node节点(tabAt(tab, (n – 1) & h)),然后根据链表或者树形方式找到相对应的节点,返回其value值。

六、扩容

 1 // 首先要说明的是,方法参数 size 传进来的时候就已经翻了倍了
 2 private final void tryPresize(int size) {
 3     // c:size 的 1.5 倍,再加 1,再往上取最近的 2 的 n 次方。
 4     int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
 5         tableSizeFor(size + (size >>> 1) + 1);
 6     int sc;
 7     while ((sc = sizeCtl) >= 0) {
 8         Node<K,V>[] tab = table; int n;
 9 
10         // 这个 if 分支和之前说的初始化数组的代码基本上是一样的,在这里,我们可以不用管这块代码
11         if (tab == null || (n = tab.length) == 0) {
12             n = (sc > c) ? sc : c;
13             if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
14                 try {
15                     if (table == tab) {
16                         @SuppressWarnings("unchecked")
17                         Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
18                         table = nt;
19                         sc = n - (n >>> 2); // 0.75 * n
20                     }
21                 } finally {
22                     sizeCtl = sc;
23                 }
24             }
25         }
26         else if (c <= sc || n >= MAXIMUM_CAPACITY)
27             break;
28         else if (tab == table) {
29             // 我没看懂 rs 的真正含义是什么,不过也关系不大
30             int rs = resizeStamp(n);
31 
32             if (sc < 0) {
33                 Node<K,V>[] nt;
34                 if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
35                     sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
36                     transferIndex <= 0)
37                     break;
38                 // 2. 用 CAS 将 sizeCtl 加 1,然后执行 transfer 方法
39                 //    此时 nextTab 不为 null
40                 if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
41                     transfer(tab, nt);
42             }
43             // 1. 将 sizeCtl 设置为 (rs << RESIZE_STAMP_SHIFT) + 2)
44             //     我是没看懂这个值真正的意义是什么?不过可以计算出来的是,结果是一个比较大的负数
45             //  调用 transfer 方法,此时 nextTab 参数为 null
46             else if (U.compareAndSwapInt(this, SIZECTL, sc,
47                                          (rs << RESIZE_STAMP_SHIFT) + 2))
48                 transfer(tab, null);
49         }
50     }
51 }

这个方法的核心在于 sizeCtl 值的操作,首先将其设置为一个负数,然后执行 transfer(tab, null),再下一个循环将 sizeCtl 加 1,并执行 transfer(tab, nt),之后可能是继续 sizeCtl 加 1,并执行 transfer(tab, nt)。

所以,可能的操作就是执行 1 次 transfer(tab, null) + 多次 transfer(tab, nt),关于这个可以查看源码,篇幅过长,这里不说了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值