我真的懂java吗(一)

这真是我所知道的Map吗?

开门先来几个问题,这样有目的研究会更清楚
1. 除了HashMap你还用过其他的Map吗?
2. HashMap底层是如何实现的?
3. Map是无序的吗?
4. ConcurrentHashMap 是如何实现线程安全的?

除了HashMap你还用过其他的Map吗?

interesting HashMap在实际中非常常用,甚至我们常常将HashMap和Map等价了。
我所知道的是 LinkedHashMap,HashTable 但是我还真没用过这2个。
那么我整理了一下Map的实现类,大约如下

graph TD
    A[ Map ] --> B[AbstractMap]
    A --> SortedMap
    SortedMap --> NavigableMap
    NavigableMap --> TreeMap
    A --> HashTable
    B --> TreeMap
    B --> HashMap
    HashMap --> LinkedHashMap
    B --> EnumMap
    B --> IdentityHashMap
    B --> WeakHashMap

WeakHashMap 还有个内部类的实现(ClassValue中的ClassValueMap)

我一直觉得自己整理出来这个,已经可以了!直到第5个问题的发生~~~~
我TM干了什么,我竟然把concurrent包忘记了,忘记了,忘记了。我仿佛听到了一个响亮的耳光!!

o(≧口≦)o
ヾ(。ꏿ﹏ꏿ)ノ゙

好吧,加上concurrentMap及它的实现ConcurrentHashMap,ConcurrentSkipListMap

graph TD
    A[ Map ] --> B[AbstractMap]
    A --> C[ConcurrentMap]
    B --> HashMap
    HashMap --> LinkedHashMap
    B --> EnumMap
    B --> IdentityHashMap
    B --> WeakHashMap
    B --> TreeMap
    C --> ConcurrentHashMap
    C --> ConcurrentNavigableMap 
    ConcurrentNavigableMap --> ConcurrentSkipListMap
    B --> ConcurrentHashMap
    B --> ConcurrentSkipListMap
    A --> HashTable
    A --> SortedMap
    SortedMap --> NavigableMap
    NavigableMap --> TreeMap

天啊,好乱!!!为了没有其他的耳光,我决定打开我珍藏的api,看看Map还有没其他实现。于是发现

Interface Map
所有已知实现类:
AbstractMap, Attributes, AuthProvider, ConcurrentHashMap, ConcurrentSkipListMap, EnumMap, HashMap, Hashtable, IdentityHashMap, LinkedHashMap, PrinterStateReasons, Properties, Provider, RenderingHints, SimpleBindings, TabularDataSupport, TreeMap, UIDefaults, WeakHashMap

Attributes
Properties –> Provider –> AuthProvider
把Attributes,Properties 丢到爪哇国的我现在只想撞墙。

PrinterStateReasons、RenderingHints、SimpleBindings、TabularDataSupport、UIDefaults是javax中的内容(rt.jar)就先不考虑了。

如上分析:最起码应该说出来的LinkedHashMap、TreeMap、Attributes、Properties 、ConcurrentHashMap、Hashtable

HashMap底层是如何实现的?

分析一下,其实这个问题包含好多个问题,HashMap的坑是一个接一个的。
1. HashMap是如何存储值的。
2. put的时候是如何确定值的位置,而且get的时候要能找得到。
3. 会不会出现2个值同时存在一个位置。
4. HashMap扩容是如何实现的。

其实如果深挖,估计还能挖出来更多,所以我决定推翻我对HashMap的理解,去看源码。
源码中实现过程如下:

transient Node<K,V>[] table;

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        //此处省略
}

上述源码中的意思比较明显,就是定义了一个Node数组。Node中需要关注的是Next,它是定义了一个链表结构。这段代码直接表述出了一个基本的拉链法结构。

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

这里就是HashMap中传说已久的hash算法,看得出来,其中借助了Object的HashCode。

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

get方法没啥,主要是getNode实现的多,getNode逻辑中竟然还包括了TreeNode的一部分实现。按道理父类不应该包含子类逻辑的(TreeNode的确是HashMap的实现,但是继承了LinkedHashMap.Entry,LinkedHashMap.Entry继承了Node。他们都是Map.Entry的实现类),请注意这里有个大坑,稍后详细分析,HashMap的链表有2种实现,一种是Node实现的单向链表,一种是TreeNode实现的树(双向链表)。

 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }
}

getNode主要就是从链表中迭代寻找key值一致的Node。hash(key)的作用比较偏向于定位数组的下标。
定位数组下标主要靠2句代码

n = tab.length
(n - 1) & hash

这里结合hash(Object key)方法中的算法可以得到(tab.length-1) & (h = key.hashCode()) ^ (h >>> 16) 这个计算数组下标的核心算法。

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            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) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);//treeMap中的实现
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);//treeMap中的实现
        return null;
    }

这个put的逻辑好麻烦,忽略掉TreeNode的实现。我们一样可以发现n = tab.length、tab[i = (n - 1) & hash]这样的语句,也就说明取数组下标的算法和get方法是一致的。
这里有几个坑,tab = resize() 、++size > threshold 、treeifyBin(tab, hash)。

这里就必须去看resize、treeifyBin这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) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            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;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                //此处省略
            }
        }
        return newTab;
    }

说实话,这个resize方法看了我一个下午,加上n多试验。然后不得不承认我的孤陋寡闻,之前一直以为resize挺简单的。在不得不吐槽这个代码的变量命名简直惨不忍睹(Cap是表示数组的长度,Thr表示的是map的预警值),而且十分苦逼的是这些注释有啥用。
这里得到一些有用的信息,就是new HashMap的时候数组是没有初始化的,只有放入第一个值才会去初始化数组。数组在MAXIMUM_CAPACITY(1<<30)的时候都是直接翻倍的(newCap = oldCap << 1)。大于MAXIMUM_CAPACITY时是警戒值加到Integer.MAX_VALUE,数组不发生改变。
tab = resize() 、++size > threshold告诉我们了初始化数组的时间(第一次put)和数组扩张的时间(已有元素大于警戒值)。

final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);
        }
    }

treeifyBin(tab, hash);的作用非常奇怪,这里代码的作用是当单个链表长度>=TREEIFY_THRESHOLD(8) - 1时将原本的Node替换为TreeNode。Node和TreeNode最大的差距在于Node只有Next是单向链表,而TreeNode存在prev和left,right是树结构,因为TreeNode是继承的Node所以也是存在next的。
此处是非常混乱了,既是一个双向链表,也是一棵树。

这里整理下要点:
1. HashMap默认初始化数组的大小为16(DEFAULT_INITIAL_CAPACITY = 1<<4)
2. HashMap会以2^n 的方式去扩容而且它的容量只能是2^n。也就是说你指定一个初始值,HashMap也会使用大于等于这个值的2的n次幂去实例化数组(最大值是是2^30)。
3. HashMap计算元素位置会使用 (tab.length-1) & (h = key.hashCode()) ^ (h >>> 16) 的方式去计算,而不是网上流传的取模法。这里有个有意思的地方(null是没有hashCode的,HashMap将null的hash值直接固定的0,也就null值一定会在数组的下标0中)
4. HashMap中的元素的下标是可以重复的,重复的下标会使用链表的方式存储(node.next)。这种方式是典型的拉链法(数组中存放链表)。这个链表初始是单向链表,长度大于等于7是会转换为树(双向链表)。
5. 数组的扩容时机(并不是HashMap的大小,链表理论上是可以无限存储的,而数组的容量是有限的)是HashMap的已有元素>= 警戒值( 最大容量*警戒阀值或者是原警戒值双倍) 。警戒阀值默认为0.75f,可以在构造函数中自定义。这种扩容的方式可以有效的减少链表的长度,也就是使查询效率变高。迭代速度变慢。我没有说插入和删除速度变慢,因为我也不知道(数组和链表的优缺点远远没有网上说的那么简单,这和内存管理有极大的关系,整理完内存管理后继续深挖!)
6. HashMap的扩容方式,新建一个数组,然后重新移动元素。也就是说,数组和链表会重新建立。这里的大问题是慢,慢就意味着在扩容的时间中去插入元素是非常危险的。
7. HashMap 数组的极限是1073741824(1<<30) ,源码中没有发现限制链表长度的代码。也就是理论上HashMap的存储并没有极限。所以,之前理解的1073741824最大值是有问题的。(我电脑内存12G给了JVM4个G,可以把map弄成1<<28。我要换电脑才能试出来Map的极限吗?)

Map是无序的吗?

这里既然问了Map,其实在第一个问题中已经有明显的回答了,LinkedHashMap和TreeMap明显不是没有结构的命名。这里就引申出一个问题,他们是如何保证自己的结构的!

此处将总结提前
1. LinkedHashMap在基本的数据存储中依旧是使用的拉链法。只是为了保证有序性实现了一个双向链表的结构。也就是说,在平时使用的时候依旧是用的HashMap的方法(这样保证了速度不会被影响太多)。只是在迭代器中使用了双向链表结构,使它可以被迭代了(其实不止迭代器)。
2. HashMap是一个数组+单向链表或树的结构,单向链表和树是在同一个数组下标上只有一种结构,所以HashMap也不是绝对无序(只要想实现还是可以进行有序迭代的)。LinkedHashMap就非常奇怪,它的链表实现是完全独立于数组的,可以理解为在HashMap的基础上增加了链表实现用来保存顺序,其顺序与保存顺序完全一致。
3. TreeMap的实现是通过红黑树实现的。在TreeMap的保存时会默认进行排序(key必须是java.lang.Comparable的实现),也就是说TreeMap的顺序和保存顺序无关(put,1、2、3、4、5、6和put,6、5、4、3、2、1得到的结果是一样的)。

LinkedHashMap

上面的源码已经把脸快打成猪头了,很多理解发现都和源码对不上,所以干脆重新理理算了。
经过,查看LinkedHashMap的源码,发现一个奇怪的事情:

LinkedHashMap本身没有实现put方法,也就是说,它是用的父类HashMap的put方法。

public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

get方法变化并不大,一样是使用了getNode(),而且这个getNode也没有被重写,依旧是HashMap中的方法。所以它的存储结构是和HashMap是一致的,那么LinkedHashMap是如何保证它的顺序的呢?

所以我们的目标应该转向get,put方法出现的3个方法中。当然还有迭代器的实现也是需要注意的。上面引用中出现了afterNodeAccess,这个方法莫名眼熟。这个方法在HashMap中的put方法出现过,同时出现的还有afterNodeInsertion,在HashMap中的removeNode中还出现了afterNodeRemoval方法。

    // Callbacks to allow LinkedHashMap post-actions
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }

注释就说明了是给LinkedHashMap回调的~
其中重头戏是afterNodeAccess,按照重头戏压轴的规则,所以这个最后说~

void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

先来看看打酱油的插入afterNodeInsertion,看名字挺重要的,实际上就是个打酱油的。因为即使在if中前2个判断都是true,最后一个removeEldestEntry已经恒定为false。为这个酱油默哀三分钟,有人知道它为啥酱油请告诉我!!!

void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

afterNodeRemoval没有打酱油,其实主要就是将入参Node从链表中摘掉了。但是这里有个非常重要的信息,那就是它只修改了before和after,所以可以知道这里是个双向链表。
这里还有个引申那就是它敢非常肯定的转型称为LinkedHashMap.Entry而不怕报错,那么我推测LinkedHashMap中的元素只可能为2种(Entry,TreeMap)。看到此处,前面HashMap中的一个混乱点就清晰了,TreeNode除了需要保证树结构来保证查询效率,还需要保存链表结构使LinkedHashMap有序。

void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

来到了我期待已久的重头戏,然后,我发现他是个假的重头戏,这个根本就是个骗子方法。这个注释move node to last简直在嘲讽我的智商。为啥这个方法叫Access?谁能告诉我这个方法为何也是个酱油,将元素放到链表最后一个有啥意义吗?

那么跳过了3个假重点后,我收获了一些草泥马草原的空气,还有一个问题。它为何干如此胆大的敢直接转型成Entry。没有重写put方法让我很放心的将问题定位在newNode和newTreeNode头上了。于是我找到了这2个方法。

 Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
        TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
        linkNodeLast(p);
        return p;
    }

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }

到这里LinkedHashMap的存储问题就已经搞定了,就是用的双向链表来保证数据的顺序的。为了防止我的智商再次被戏耍,所以我还是看看迭代器去。

abstract class LinkedHashIterator {
        LinkedHashMap.Entry<K,V> next;
        LinkedHashMap.Entry<K,V> current;
        int expectedModCount;

        LinkedHashIterator() {
            next = head;
            expectedModCount = modCount;
            current = null;
        }

        public final boolean hasNext() {return next != null;}

        final LinkedHashMap.Entry<K,V> nextNode() {
            LinkedHashMap.Entry<K,V> e = next;
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            current = e;
            next = e.after;
            return e;
        }

        public final void remove() {
           //此处省略
        }
    }

看到了迭代器中的next = e.after;,在看迭代器的三个实现并没有重写这个方法我觉得世界清静了。

TreeMap

恩,上面HashMap对我认知的颠覆最大,我一直以为HashMap是数组+双向链表,谁知道竟然是单向链表和树。
不扯了,继续看TreeMap。根据上面的经验,先看三个地方Map.Entry的实现,put,get方法。

static final class Entry<K,V> implements Map.Entry<K,V> {
        K key;
        V value;
        Entry<K,V> left;
        Entry<K,V> right;
        Entry<K,V> parent;
        boolean color = BLACK;

        //此处省略
    }

    public V put(K key, V value) {
        Entry<K,V> t = root;
        if (t == null) {
            compare(key, key); // type (and possibly null) check

            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
                Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }

    public V get(Object key) {
        Entry<K,V> p = getEntry(key);
        return (p==null ? null : p.value);
    }

    final Entry<K,V> getEntry(Object key) {
        // Offload comparator-based version for sake of performance
        if (comparator != null)
            return getEntryUsingComparator(key);
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        Entry<K,V> p = root;
        while (p != null) {
            int cmp = k.compareTo(p.key);
            if (cmp < 0)
                p = p.left;
            else if (cmp > 0)
                p = p.right;
            else
                return p;
        }
        return null;
    }

这里可以获取到一个信息,就是TreeMap的key不能是随意的,必须是java.lang.Comparable的子类。没有compareTo方法貌似TreeMap就转不动了。
根据Entry我们基本可以判断这是一棵红黑树了,在put方法最后面调用了一次fixAfterInsertion,这个方法实现了平衡红黑树的功能。不出所料,我在remove方法看到调用了fixAfterDeletion。

到这里就没有啥研究欲望了,毕竟红黑树结构,和红黑树的平衡都有了。最后看下迭代器避免翻车。

abstract class PrivateEntryIterator<T> implements Iterator<T> {
        Entry<K,V> next;
        //此处省略
        final Entry<K,V> nextEntry() {
            Entry<K,V> e = next;
            if (e == null)
                throw new NoSuchElementException();
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            next = successor(e);
            lastReturned = e;
            return e;
        }
        //此处省略
}
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
        if (t == null)
            return null;
        else if (t.right != null) {
            Entry<K,V> p = t.right;
            while (p.left != null)
                p = p.left;
            return p;
        } else {
            Entry<K,V> p = t.parent;
            Entry<K,V> ch = t;
            while (p != null && ch == p.right) {
                ch = p;
                p = p.parent;
            }
            return p;
        }
    }

从上面看到下个节点的读取顺序是左中右,是中序。也就是是说第一个节点是在树的最左侧的子节点。于是我找到了这么一段。

    final Entry<K,V> getFirstEntry() {
        Entry<K,V> p = root;
        if (p != null)
            while (p.left != null)
                p = p.left;
        return p;
    }

ConcurrentMap是如何实现线程安全的

ConcurrentMap有2个实现,一个是ConcurrentHashMap,这个是要对比HashMap去看看的。另一个是ConcurrentSkipListMap,这个就没啥参照物了,慢慢看咯。

ConcurrentHashMap的线程安全

读HashMap源码给了很多经验,比如我们需要关注的点已经非常清晰了数组的定义、Map.Entry的实现、put、get、remove。
先看看数组

private static final int MAXIMUM_CAPACITY = 1 << 30;
private static final int DEFAULT_CAPACITY = 16;
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private static final float LOAD_FACTOR = 0.75f;

transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;

一开始的初始定义和HashMap并没有什么区别,都是默认初始16,最大1<<30。但是有个Integer.MAX_VALUE - 8的疑惑在,回头再看看这个定义应用在那里。
好了,重点来了,table的定义中比HashMap多了个volatile关键字,而且还多了个nextTable。volatile的作用在这个地方应该是保证table在内存中的可见性。nextTable的作用就不清楚了,继续往下看了。

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;

        Node(int hash, K key, V val, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.val = val;
            this.next = next;
        }

        public final K getKey()       { return key; }
        public final V getValue()     { return val; }
        public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
        public final String toString(){ return key + "=" + val; }
        public final V setValue(V value) {
            throw new UnsupportedOperationException();
        }
}

Node定义中有2个点和HashMap区别较大,一个是val和next都是用了volatile关键字,另一个是setValue直接出异常:不支持此操作。

public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        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;
}

get方法貌似没啥特点,虽然说和HashMap完全不一样,但是也没啥可以关注并得到信息的地方(可能是我水平不够)。

public V put(K key, V value) {
        return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        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;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> 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<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) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}
public V remove(Object key) {
    return replaceNode(key, null, null);
}

/**
* Implementation for the four public remove/replace methods:
* Replaces node value with v, conditional upon match of cv if
* non-null.  If resulting value is null, delete.
*/
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) {
                        //此处省略
                    }
                    else if (f instanceof TreeBin) {
                        //此处省略
                    }
                }
            }
            //此处省略
        }
    }
    return null;
}

(@ο@) 哇~,这2个方法看的我头好大。整理整理
1. 和HashMap一样,链表中存在2中结构,一种是单向链表一种是树。不同的是这里用的不是TreeNode而是TreeBin
2. synchronized (f) 加锁的对象是数组的节点,而不是整个数组。
3. e.val = value; 说明了不是不支持setValue操作,而是直接修改了值。为啥?
大约就这些,多线程中需要注意的事情原子性,可见性,有序性。对Node节点加锁的方法保证对单个节点的操作是原子的,volatile保证了可见性(有一定程度上的有序)。

多线程这块一直是弱项,这里代码读的好累。特别是锁那块,一直以为remove只锁一个节点有问题,就是如果e.next被删除了会出现一些奇奇怪怪的问题。后来反应过来有volatile,修改了会及时变化的。

ConcurrentSkipListMap 如何实现线程安全

说实话,这个代码看疯我了,因为整个代码中根本没有出现过synchronized关键字。为何如此神奇着要归功于一个神奇的数据结构Skip List(跳表)。所以呢!它关键在于这代码的注释与数据结构。

     * Head nodes          Index nodes
     * +-+    right        +-+                      +-+
     * |2|---------------->| |--------------------->| |->null
     * +-+                 +-+                      +-+
     *  | down              |                        |
     *  v                   v                        v
     * +-+            +-+  +-+       +-+            +-+       +-+
     * |1|----------->| |->| |------>| |----------->| |------>| |->null
     * +-+            +-+  +-+       +-+            +-+       +-+
     *  v              |    |         |              |         |
     * Nodes  next     v    v         v              v         v
     * +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+
     * | |->|A|->|B|->|C|->|D|->|E|->|F|->|G|->|H|->|I|->|J|->|K|->null
     * +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+  +-+
static final class Node<K,V> {
        final K key;
        volatile Object value;
        volatile Node<K,V> next;
        //此处省略
}
static class Index<K,V> {
        final Node<K,V> node;
        final Index<K,V> down;
        volatile Index<K,V> right;
        //此处省略
}

好了,上面代码和注释只需要表示ConcurrentSkipListMap使用跳表实现就可以了,跳表+volatile为何能保证线程安全,后续在整理一下。(我那可怜的数据结构已经完全还给老师了,我需要十全大补汤~~~~)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值