上一篇文章简单的写了一下,BoundedConcurrentHashMap,觉得https://www.cnblogs.com/qiaoyutao/p/10903813.html用的并不多;今天着重写一下ConcurrentHashMap几个常用方法的源码理解,
注: 本文章纯属个人理解,对阅读者只作参考。
先来看构造器
public ConcurrentHashMap(Map<? extends K, ? extends V> m) { this.sizeCtl = DEFAULT_CAPACITY; // 将表的长度设置为默认的DEFAULT_CAPCITY=16; putAll(m); // 调用方法将传进来的map构建成一个线程安全的hash表,下面是putAll的源码 }
public void putAll(Map<? extends K, ? extends V> m) { tryPresize(m.size()); // 根据传入的map表的大小来尝试调整此map表的sizeCtl的大小,并创建一个m.size长度的Node<k,v>[]数组给成员变量table。 for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) // 遍历集合放入上面的Node数组中。 下面是putVal方法 putVal(e.getKey(), e.getValue(), false); }
final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); // 不允许key或者value为null int hash = spread(key.hashCode()); // 对key进行再hash int binCount = 0; // 用来计算在这个结点总共有多少个元素, 以此来判断是否需要扩容或者转化为红黑树 for (Node<K,V>[] tab = table;;) { // 此处将Node数组table赋值给tab Node<K,V> f; int n, i, fh; if (tab == null || (n = tab.length) == 0) // 如果tab也就是Node数组为空那么就进行初始化 tab = initTable(); // initTable方法源码在下面 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // tabAt用来取出tab中(n-1)&hash 下标下的Node结点 if (casTabAt(tab, i, null, // 如果i对应的位置若为null 那么就用cas的方式将此方法的第三个参数添加进来。 这里是通过cas操作的没有加锁 new Node<K,V>(hash, key, value, null))) // 这里的参数null代表新创建的Node的下一个元素为null break; // no lock when adding to empty bin } else if ((fh = f.hash) == MOVED) /// 当有结点的hash值等于MOVED时,说明该Node数组结点下的hash表正在扩容,那么就帮助扩容(多线程扩容,helpTransfer方法就是帮助扩容) tab = helpTransfer(tab, f); else {
// 进入else 说明该key下有结点,那么就加锁进行操作,如果fh也即是该结点的hash如果大于0说明是一个链表,就循环遍历这个链表并比较key,若期间有key相等的就将value替换,若没有就新创建一个Node节点添加到
// 链表结尾;否则就判断该Node节点是不是TreeBin(红黑树)类型的,就调用putTreeVal将值替换或添加进红黑树中, putTreeVal方法见下面。 当一个结点下的数量超过8时,就将此Hash表转换为红黑树 V oldVal = null; synchronized (f) {// 对该Node结点进行加锁 if (tabAt(tab, i) == f) { // 取出该结点与前面if中取出的结点比较,如果相同那么说明期间没有别的线程进行操作该结点。 if (fh >= 0) { // 该结点的hash大于0时说明是链表 binCount = 1; for (Node<K,V> e = f;; ++binCount) { // 遍历该结点下的hash表 K ek; if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // 比较key和key的hash是否相同,如果相同那么就替换value oldVal = e.val; if (!onlyIfAbsent) e.val = value; break; } Node<K,V> pred = e; if ((e = e.next) == null) { // 如果没有key相同的Node结点,那么就创建一个并添加到hash表的结尾 pred.next = new Node<K,V>(hash, key, value, null); break; } } } else if (f instanceof TreeBin) { // 如果该Node数组结点下的Node结点是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) // 当binCount(结点下的hash表的元素数量大于等于TREEIFY_THRESHOLD(8)时,就转换为红黑树) treeifyBin(tab, i); if (oldVal != null) return oldVal; break; } } } addCount(1L, binCount); return null; }
private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { // 判断Node数组是否为空,不为空的话说明hash表已经初始化。 if ((sc = sizeCtl) < 0) // 当前sizeCtl < 0时,说明该数组正在被别的线程操作(或者扩容,或者初始化), 那么就用Thread.yield挂起线程 Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
/** U.compareAndSwapInt(this,SIZECTL,sc,-1) 是java的Unsafe类的方法,使用了CAS算法来保证操作的原子性;何为CAS也即比较并替换,这里只大概的讲解,具体请参考https://www.jianshu.com/p/fb6e91b013cc
* CAS是根据内存地址来判断值有没有被修改, 于是this:表示被操作的对象,SIZECTL:是当前的sizeCtl的值,sc是sizeCtl的旧值,-1是即将要替换成的值。 compareAndSwapInt先会比较SIZECTL的值与sizeCtl是否相同如果
* 相同说明在期间没有操作hash表,就把当前sizeCtl的值改为-1,并返回true,告诉其他线程,有线程正在初始化hash表。如果不相同说明有线程操作正在初始化hash表,其他线程就会等待挂起。
try { if ((tab = table) == null || tab.length == 0) { // 如果Node数组还没有初始化 int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 确定Node数组的大小。 @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; // 创建数组 table = tab = nt; // 将数组给table和tab,给tab的目的是告诉其他线程,hash表已经初始化。 sc = n - (n >>> 2); } } finally { sizeCtl = sc; // 将数组长度无符号右移位2的次幂更新到sizeCtl } break; } } return tab; }
其他的put方法也都是调用了pubVal(), 至于get等方法就不用写了,也就是比较key取value。