HashtableConcurrentHashMap源码分析

Posted best.lei

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HashtableConcurrentHashMap源码分析相关的知识,希望对你有一定的参考价值。

Hashtable、ConcurrentHashMap源码分析

  为什么把这两个数据结构对比分析呢,相信大家都明白。首先二者都是线程安全的,但是二者保证线程安全的方式却是不同的。废话不多说了,从源码的角度分析一下两者的异同,首先给出二者的继承关系图。

 Hashtable类属性和方法源码分析

  我们还是先给出一张Hashtable类的属性和方法图,其中Entry<K,V>是Hashtable类的静态内部类,该类继承自Map.Entry<K,V>接口。如下将会详细讲解Hashtable类中属性和方法的含义。

  • 属性
  1. Entry<?,?>[] table :Entry类型的数组,用于存储Hashtable中的键值对;
  2. int count :存储hashtable中有多少个键值对
  3. int threshold :当count值大于该值是,哈希表扩大容量,进行rehash()
  4. float loadFactor :threshold=哈希表的初始大小*loadFactor,初始容量默认为11,loadFactor值默认为0.75
  5. int modCount :实现"fail-fast"机制,在并发集合中对Hashtable进行迭代操作时,若其他线程对Hashtable进行结构性的修改,迭代器会通过比较expectedModCount和modCount是否一致,如果不一致则抛出ConcurrentModificationException异常。如下通过一个抛出ConcurrentModificationException异常的例子说明。
    public static void main(String[] args) {
             Hashtable<Integer, String> tb = new Hashtable<Integer,String>();
             tb.put(1, "BUPT");
             tb.put(2, "PKU");
             tb.put(3, "THU");
             Iterator<Entry<Integer, String>> iter = tb.entrySet().iterator();
             while(iter.hasNext()){
                 Entry<?, ?> entry = (Entry<?, ?>) iter.next(); //此处会抛出异常
                 System.out.println(entry.getValue());
                 if("THU".equals(entry.getValue())){
                     tb.remove(entry.getKey());
                 }
             }
        }
    /* 输出结果如下:
    THU
    Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.Hashtable$Enumerator.next(Hashtable.java:1367)
        at ali.Main.main(Main.java:16) */
    ConcurrentModificationException异常

    Hashtable的remove(Object key)方法见如下方法5,每一次修改hashtable中的数据都更新modCount的值。Hashtable内部类Enumerator<T>的相关部分代码如下:

        private class Enumerator<T> implements Enumeration<T>, Iterator<T> {
            Entry<?,?>[] table = Hashtable.this.table;
            int index = table.length;
            Entry<?,?> entry;
            Entry<?,?> lastReturned;
            int type;
    
            /**
             * Indicates whether this Enumerator is serving as an Iterator
             * or an Enumeration.  (true -> Iterator).
             */
            boolean iterator;
    
            /**
             * 遍历之初将hashtable修改的次数赋值给expectedModCount
             */
            protected int expectedModCount = modCount;
    
            Enumerator(int type, boolean iterator) {
                this.type = type;
                this.iterator = iterator;
            }
            //
            public boolean hasMoreElements() {
                Entry<?,?> e = entry;
                int i = index;
                Entry<?,?>[] t = table;
                /* Use locals for faster loop iteration */
                while (e == null && i > 0) {
                    e = t[--i];
                }
                entry = e;
                index = i;
                return e != null;
            }
    
            @SuppressWarnings("unchecked")
            public T nextElement() {
                Entry<?,?> et = entry;
                int i = index;
                Entry<?,?>[] t = table;
                /* Use locals for faster loop iteration */
                while (et == null && i > 0) {
                    et = t[--i];
                }
                entry = et;
                index = i;
                if (et != null) {
                    Entry<?,?> e = lastReturned = entry;
                    entry = e.next;
                    return type == KEYS ? (T)e.key : (type == VALUES ? (T)e.value : (T)e);
                }
                throw new NoSuchElementException("Hashtable Enumerator");
            }
    
            //查看是否还有下一个元素
            public boolean hasNext() {
                return hasMoreElements();
            }
    
            public T next() {
                //首先判断modCount和expectedModCount是否相等
                //由于在主程序中Hashtable对象通过tb.remove()方法修改了modCount的值,使得expectedModCount和modCount不相等而抛出异常
                //解决办法就是将tb.remove()方法替换为iter.remove()方法
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
                return nextElement();
            }
            //该方法在remove元素的同时修改了modCount和expectedModCount的值
            public void remove() {
                if (!iterator)
                    throw new UnsupportedOperationException();
                if (lastReturned == null)
                    throw new IllegalStateException("Hashtable Enumerator");
                if (modCount != expectedModCount)
                    throw new ConcurrentModificationException();
    
                synchronized(Hashtable.this) {
                    Entry<?,?>[] tab = Hashtable.this.table;
                    int index = (lastReturned.hash & 0x7FFFFFFF) % tab.length;
    
                    @SuppressWarnings("unchecked")
                    Entry<K,V> e = (Entry<K,V>)tab[index];
                    for(Entry<K,V> prev = null; e != null; prev = e, e = e.next) {
                        if (e == lastReturned) {
                            modCount++;
                            expectedModCount++;
                            if (prev == null)
                                tab[index] = e.next;
                            else
                                prev.next = e.next;
                            count--;
                            lastReturned = null;
                            return;
                        }
                    }
                    throw new ConcurrentModificationException();
                }
            }
        }
    Enumerator类
  • 方法
  1. contains(Object value),该方法是判断该hashtable中是否含有值为value的键值对,执行该方法需要加锁(synchronized)。hashtable中不允许存储空的value,所以当查找value为空时,直接抛出空指针异常。接下来是两个for循环遍历table。由如上的Entry实体类中的属性可以看出,next属性是指向与该实体拥有相同hashcode的下一个实体。
  2. containsKey(Object key),该方法是判断该hashtable中是否含有键为key的键值对,执行该方法也需要对整张table加锁(synchronized)。首先根据当前给出的key值计算hashcode,并有hashcode值计算该key所在table数组中的下标,依次遍历该下标中的每一个Entry对象e。由于不同的hashcode映射到数组中下标的位置可能相同,因此首先判断e的hashcode值和所查询key的hashcode值是否相同,如果相同在判断key是否相等。
  3. get(Object key),获取当前键key所对应的value值,本方法和containsKey(Object key)方法除了返回值其它都相同,如果能找到该key对应的value,则返回value的值,如果不能则返回null。

  4.  put(K key, V value),将该键值对加入table中。首先插入的value不能为空。其次如果当前插入的key值已经在table中存在,则用新的value替换掉原来的value值,并将原来的value值作为该方法的返回值返回。如果当前插入的key不在table中,则将该键值对插入。

    插入的方法首先判断当前table中的值是否大于阈值(threshold),如果大于该阈值,首先对该表扩容,再将新的键值对插入table[index]的链表的第一个Entry的位置上。

  5. remove(Object key),将键为key的Entry从table表中移除。同样该方法也需要锁定整个table表。如果该table中存在该键,则返回删除的key的value值,如果当前table中不存在该key,则该方法的返回值为null。
  6. replace(K key, V value),将键为key的Entry对象值更新为value,并将原来的value最为该方法的返回值。

ConcurrentHashMap类属性和方法源码分析

  ConcurrentHashMap在JDK1.8中改动还是挺大的。它摒弃了Segment(段锁)的概念,在实现上采用了CAS算法。底层使用数组+链表+红黑树的方式,但是为了做到并发,同时也增加了大量的辅助类。如下是ConcurrentHashMap的类图。

  • 属性
//ConcurrentHashMap最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;

//ConcurrentHashMap初始默认容量
private static final int DEFAULT_CAPACITY = 16;

//最大table数组的大小
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

//默认并行级别,主体代码并未使用
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;

//加载因子,默认为0.75
private static final float LOAD_FACTOR = 0.75f;

//当hash桶中hash冲突的数目大于此值时,将链表转化为红黑树,加快hash的查找速度
static final int TREEIFY_THRESHOLD = 8;

//当hash桶中hash冲突小于等于此值时,会把红黑树转化为链表
static final int UNTREEIFY_THRESHOLD = 6;

//当table数组的长度大于该值时,同时满足hash桶中hash冲突大于TREEIFY_THRESHOLD时,才会把链表转化为红黑树
static final int MIN_TREEIFY_CAPACITY = 64;

//扩容操作中,transfer()方法允许多线程,该值表示一个线程执行transfer时,至少对连续的多少个hash桶进行transfer
private static final int MIN_TRANSFER_STRIDE = 16;

//ForwardingNode的hash值,ForwardingNode是一种临时节点,在扩容中才会出现,不存储实际的数据
static final int MOVED     = -1;

//TreeBin的hash值,TreeBin是用于代理TreeNode的特殊节点,存储红黑树的根节点
static final int TREEBIN   = -2;

//用于和负数hash进行&运算,将其转化为正数
static final int HASH_BITS = 0x7fffffff;
  •  基本类
  1. Node<K,V>:基本结点/普通节点。当table中的Entry以链表形式存储时才使用,存储实际数据。此类不会在ConcurrentHashMap以外被修改,而且该类的key和value永远不为null(其子类可为null,随后会介绍)。
    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; }
            //不支持直接设置value的值
            public final V setValue(V value) {
                throw new UnsupportedOperationException();
            }
    
            public final boolean equals(Object o) {
                Object k, v, u; Map.Entry<?,?> e;
                return ((o instanceof Map.Entry) &&
                        (k = (e = (Map.Entry<?,?>)o).getKey()) != null &&
                        (v = e.getValue()) != null &&
                        (k == key || k.equals(key)) &&
                        (v == (u = val) || v.equals(u)));
            }
    
           //从当前节点查找对应的键为k的Node<K,V>
            Node<K,V> find(int h, Object k) {
                Node<K,V> e = this;
                if (k != null) {
                    do {
                        K ek;
                        if (e.hash == h &&
                            ((ek = e.key) == k || (ek != null && k.equals(ek))))
                            return e;
                    } while ((e = e.next) != null);
                }
                return null;
            }
        }
    Node<K,V>
  2. TreeNode:红黑树结点。当table中的Entry以红黑树的形式存储时才会使用,存储实际数据。ConcurrentHashMap中对TreeNode结点的操作都会由TreeBin代理执行。当满足条件时hash会由链表变为红黑树,但是TreeNode中通过属性prev依然保留链表的指针。
    static final class TreeNode<K,V> extends Node<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,
                     TreeNode<K,V> parent) {
                super(hash, key, val, next);
                this.parent = parent;
            }
    
            Node<K,V> find(int h, Object k) {
                return findTreeNode(h, k, null);
            }
    
            //查找hashcode为h,key为k的TreeNode结点
            final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
                if (k != null) {
                    TreeNode<K,V> p = this;
                    do  {
                        int ph, dir; K pk; TreeNode<K,V> q;
                        TreeNode<K,V> pl = p.left, pr = p.right;
                        if ((ph = p.hash) > h)
                            p = pl;
                        else if (ph < h)
                            p = pr;
                        else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
                            return p;
                        else if (pl == null)
                            p = pr;
                        else if (pr == null)
                            p = pl;
                        else if ((kc != null ||
                                  (kc = comparableClassFor(k)) != null) &&
                                 (dir = compareComparables(kc, k, pk)) != 0)
                            p = (dir < 0) ? pl : pr;
                        else if ((q = pr.findTreeNode(h, k, kc)) != null)
                            return q;
                        else
                            p = pl;
                    } while (p != null);
                }
                return null;
            }
        }
    TreeNode<K,V>
  3. ForwardingNode:转发结点。该节点是一种临时结点,只有在扩容进行中才会出现,其为Node的子类,该节点的hash值固定为-1,并且他不存储实际数据。如果旧table的一个hash桶中全部结点都迁移到新的数组中,旧table就在桶中放置一个ForwardingNode。当读操作或者迭代操作遇到ForwardingNode时,将操作转发到扩容后新的table数组中去执行,当写操作遇见ForwardingNode时,则尝试帮助扩容。
    static final class ForwardingNode<K,V> extends Node<K,V> {
            final Node<K,V>[] nextTable;
            //构造函数指定hash值为MOVED,key=null, value=null, next=null
            ForwardingNode(Node<K,V>[] tab) {
                super(MOVED, null, null, null);
                this.nextTable = tab;
            }
    
            Node<K,V> find(int h, Object k) {
                //for循环避免多次遇见ForwardingNode导致递归过深
                outer: for (Node<K,V>[] tab = nextTable;;) {
                    Node<K,V> e; int n;
                    if (k == null || tab == null || (n = tab.length) == 0 ||
                        (e = tabAt(tab, (n - 1) & h)) == null)
                        return null;
                    for (;;) {
                        int eh; K ek;
                        if ((eh = e.hash) == h &&
                            ((ek = e.key) == k || (ek != null && k.equals(ek))))
                            return e;
                        if (eh < 0) {
                            //如果遇见ForwardingNode结点,则遍历ForwardingNode的nextTable结点
                            if (e instanceof ForwardingNode) {
                                tab = ((ForwardingNode<K,V>)e).nextTable;
                                continue outer;
                            }
                            else
                                return e.find(h, k);
                        }
                        if ((e = e.next) == null)
                            return null;
                    }
                }
            }
        }
    ForwardingNode<K,V>

    补充图一张说明扩容下是如何遍历结点的。

  4. TreeBin:代理操作TreeNode结点。该节点的hash值固定为-2,存储实际数据的红黑树的根节点。因为红黑树进行写入操作整个树的结构可能发生很大变化,会影响到读线程。因此TreeBin需要维护一个简单的读写锁,不用考虑写-写竞争的情况。当然并不是全部的写操作都需要加写锁,只有部分put/remove需要加写锁。
    static final class TreeBin<K,V> extends Node<K,V> {
            TreeNode<K,V> root;     //红黑树的根节点
            volatile TreeNode<K,V> first;    //链表的头结点
            volatile Thread waiter;    //最近一个设置waiter标志位的线程
            volatile int lockState;    //全局的锁状态
            // values for lockState
            static final int WRITER = 1; // set while holding write lock   写锁状态
            static final int WAITER = 2; // set when waiting for write lock  等待获取写锁的状态
            static final int READER = 4; // increment value for setting read lock  读锁状态,读锁可以叠加,即红黑树可以并发读,每增加一个读线程lockState的值加READER
    
            /**
             * 红黑树的读锁状态和写锁状态是互斥的,但是读写操作实际上可以是不互斥的
             * 红黑树的读写状态互斥是指以红黑树的方式进行读写操作时互斥的
             * 当线程持有红黑树的写锁时,读线程不能以红黑树的方式进行读取操作,但可以用简单链表的方式读取,从而实现了读写操作的并发执行
             * 当有线程持有红黑树的读锁时,写线程会阻塞,但是红黑树查找速度快,因此写线程阻塞时间短。
             * put/remove/replace方法会锁住TreeBin节点,因此不会出现写-写竞争。
             */
            //当hashCode相等且不是Comparable类时使用此方法判断大小
            static int tieBreakOrder(Object a, Object b) {
                int d;
                if (a == null || b == null ||
                    (d = a.getClass().getName().
                     compareTo(b.getClass().getName())) == 0)
                    d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
                         -1 : 1);
                return d;
            }
    
           //以b为头节点的链表创建红黑树
            TreeBin(TreeNode<K,V> b) {
                super(TREEBIN, null, null, null);
                this.first = b;
                TreeNode<K,V> r = null;
                for (TreeNode<K,V> x = b, next; x != null; x = next) {
                    next = (TreeNode<K,V>)x.next;
                    x.left = x.right = null;
                    if (r == null) {
                        x.parent = null;
                        x.red = false;
                        r = x;
                    }
                    else {
                        K k = x.key;
                        int h = x.hash;
                        Class<?> kc = null;
                        for (TreeNode<K,V> p = r;;) {
                            int dir, ph;
                            K pk = p.key;
                            if ((ph = p.hash) > h)
                                dir = -1;
                            else if (ph < h)
                                dir = 1;
                            else if ((kc == null &&
                                      (kc = comparableClassFor(k)) == null) ||
                                     (dir = compareComparables(kc, k, pk)) == 0)
                                dir = tieBreakOrder(k, pk);
                                TreeNode<K,V> xp = p;
                            if ((p = (dir <= 0) ? p.left : p.right) == null) {
                                x.parent = xp;
                                if (dir <= 0)
                                    xp.left = x;
                                else
                                    xp.right = x;
                                r = balanceInsertion(r, x);
                                break;
                            }
                        }
                    }
                }
                this.root = r;
                assert checkInvariants(root);
            }
    
            /**
             * 红黑树重构时西药对根节点加写锁
             */
            private final void lockRoot() {
                //尝试获取一次锁
                if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER))
                    contendedLock(); //直到获取到写锁,该方法才返回
            }
    
            /**
             * 释放写锁
             */
            private final void unlockRoot() {
                lockState = 0;
            }
    
            /**
             * 阻塞写线程,当写线程获取写锁时返回
             *因为ConcurrentHashMap的put/remove/replace方法会对TreeBin加锁,因此不会出现写-写竞争
             *因此该方法只用考虑读锁线程阻碍线程获取写锁,而不用考虑写锁线程阻碍线程获取写锁,不用考虑写-写竞争
             */
            private final void contendedLock() {
                boolean waiting = false;
                for (int s;;) {
                    //~WAITER表示反转WAITER,当没哟线程持有读锁时,该条件为true
                    if (((s = lockState) & ~WAITER) == 0) {
                        if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {
                            //没有任何线程持有读写锁时,尝试让当前线程获取写锁,同时清空waiter标识位
                            if (waiting)
                                waiter = null;
                            return;
                        }
                    }
                    else if ((s & WAITER) == 0) {   //当前线程持有读锁,并且当前线程不是WAITER状态时,该条件为true
                        if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) {   //尝试占据WAITER标识位
                            waiting = true;    //表明自己处于waiter状态
                            waiter = Thread.currentThread();
                        }
                    }
                    else if (waiting)  //当前线程持有读锁,并且当前线程处于waiter状态时,该条件为true
                        LockSupport.park(this);  //阻塞自己
                }
            }
    
            /**
             * 从根节点开始查找,找不到返回null
             * 当有写线程加上写锁时,使用链表方式进行查找
             */
            final Node<K,V> find(int h, Object k) {
                if (k != null) {
                    for (Node<K,V> e = first; e != null; ) {
                        int s; K ek;
                        //两种特殊情况下以链表的方式进行查找
                        //1、有线程正持有 写锁,这样做能够不阻塞读线程
                        //2、WAITER时,不再继续加 读锁,能够让已经被阻塞的写线程尽快恢复运行,或者刚好让某个写线程不被阻塞
                        if (((s = lockState) & (WAITER|WRITER)) != 0) {
                            if (e.hash == h &&
                                ((ek = e.key) == k || (ek != null && k.equals(ek))))
                                return e;
                            e = e.next;
                        }
                        // 读线程数量加1,读状态进行累加
                        else if (U.compareAndSwapInt(this, LOCKSTATE, s,
                                                     s + READER)) {  
                            TreeNode<K,V> r, p;
                            try {
                                p = ((r = root) == null ? null :
                                     r.findTreeNode(h, k, null));
                            } finally {
                                Thread w;
                                // 如果这是最后一个读线程,并且有写线程因为 读锁 而阻塞,那么要通知它,告诉它可以尝试获取写锁了
                                i

    以上是关于HashtableConcurrentHashMap源码分析的主要内容,如果未能解决你的问题,请参考以下文章