今天咱们就不大战秃头老了,给自己充充电,日后再战秃头老!
注:ConcurrentHashMap聊的是1.8之后的!
兄弟姐妹们都说HashMap线程不安全,想线程安全就用ConcurrentHashMap,那为什么它就线程安全那?啊?为什么呢!不用想,肯定从put()方法看起来嘛!那有请put()老哥,出来溜达一下呗!
// 哎呦,老面孔了,跟HashMap一样都调用了putVal方法 public V put(K key, V value) { return putVal(key, value, false); } // 哎呦,final修饰的方法,不允许被重写,看来很自信呀! final V putVal(K key, V value, boolean onlyIfAbsent) { // 传递值非空校验 if (key == null || value == null) throw new NullPointerException(); // 通过 key 的 hashcode 求出 hash 值,即元素所在位置 int hash = spread(key.hashCode()); int binCount = 0; // 遍历table for (Node[] tab = table;;) { Node f; int n, i, fh; // 判断是否需要初始化 if (tab == null || (n = tab.length) == 0) tab = initTable(); // 如果当前要put的位置为空,则通过 cas+自旋 方式放入,不加锁,成功后跳出循环 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node (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 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 pred = e; if ((e = e.next) == null) { pred.next = new Node (hash, key, value, null); break; } } } else if (f instanceof TreeBin) { // 红黑树,通过树的方式来添加元素 Node p; binCount = 2; if ((p = ((TreeBin )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; }
综上所述,就是加锁了!简单的描述下:
1:通过hashcode求出key应该所放的位置
2:是否已经初始化,如果没有则调用initTable()
3:当前位置的值是否为空,为空的则通过CAS乐观锁的方式添加元素(兄弟,别慌,后面会说CAS)
4:判断是否需要扩容
5:加synchronized来添加元素,如果是链表则遍历链表添加元素,如果是红黑树则调用红黑树添加元素的方法!
既然我们知道了为啥线程安全了,那我们顺带看一下initTable()方法呗,看不了上当,看不了吃亏!
private final Node[] initTable() { Node [] tab; int sc; // 是否需要初始化 while ((tab = table) == null || tab.length == 0) { // sizeCtl 这个玩意老牛了 // 如果 = -1 说明有其他线程在初始化, // -N 说明N-1个线程在扩容 // >0 代表容量 if ((sc = sizeCtl) < 0) // 既然,有其他线程在初始化,那咱们就礼让一下呗! Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {// 通过cas方式进行判断是否要初始化!(又是CAS,好奇不!) try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node [] nt = (Node [])new Node,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { // 把容量大小赋值给sizeCtl,是不是说明 >0 代表容量 sizeCtl = sc; } break; } } return tab; }
简单说就是,如果需要初始化,首先看看有没有其他线程在初始化,有就礼让,没有就让我来!特别注意sizeCtl!
看到两次CAS了,是不得说说,诶!先不说,先看看get(),这个函数很常用,顺带看下,按住躁动的心,get()非常简单!
public V get(Object key) { Node[] tab; Node e, p; int n, eh; K ek; // 求key所在的文职 int h = spread(key.hashCode()); // 是否已经初始化过了 if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { // 指定元素的hash如果和头节点的hash相同,就直接返回头节点的值 if ((eh = e.hash) == h) { if ((ek = e.key) == key || (ek != null && key.equals(ek))) return e.val; } // 如果头节点 hash小于0,则不是在扩容,就是红黑树 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; }
简单描述下:
1:通过key的hashcode求key的位置
2:判断是不是头节点,是就直接返回
3:如果头节点的hash小于0,不是在扩容,就是红黑树,通过find查找
4:最后就是链表了,遍历查找!
终于说完了ConcurrentHashMap,那是不是逼逼一下CAS那,害,咱们就放到下次充电呗!
嗯…想到看官老爷们,连个赞都不留下, 只有默默的阅读量,哎,看官老爷来个赞把!,下次咱们说说CAS,保证通俗易懂哦!(主要是我还没掌握透彻,等掌握透彻再来哦!)
参考文章:Guide哥的Java学习
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)