Collections 浅析(一)

来源:互联网 发布:中国软件学校 编辑:程序博客网 时间:2024/05/22 13:06

Collections 浅析(一)

先来几个问题?

  1. fail-fast机制是什么?
  2. CAS(Compare And Swap)是啥?
  3. ConcurrentHashMap实现原理?

先从一个熟知的问题进入:HashTable、HashMap的区别。

一、HashMap & HashTable

  1. HashTable线程安全,HashMap线程不安全。(HashMap中的方法没有使用synchronize修饰)。多个线程可以共享一个HashTable,而HashMap如果外部没有做同步的话,是不可以共享的。 (ps:Java 5提供了ConcurrentHashMap, 这HashTable的替代品,比HashTable的扩展性好)。HashMap可以通过以下方式进行同步:Map m = Collections.synchronizedMap(hashMap);
  2. HashTable不允许 K、V为null,HashMap允许。
  3. HashTable只有contains method,HashMap抛弃了contains方法,新增containsKey & containsValue方法
  4. HashTable**继承Dictionary类,实现Map接口和Serializable接口(据说是java4重写改成这样的,具体的也不得而知了);HashMap继承AbstractMap类,也实现了Map接口和Seriablizable接口,同时实现了Cloneable接口**。AbstractMap是实现Map接口的类。

ps: 网上看到有人说,HashMap的迭代器Iterator支持fail-fast,HashTable的不支持。然而,我在HashTable的源码中看到了也存在modCount这一参数。再说,HashTable本身方法都是有synchronize修饰的,线程安全的,也不需要这个机制。。。但是方法中还真的有modCount,还真判断了modCount == mc ,不等于就跑fail-fast异常。

    /**     * The number of times this HashMap has been structurally modified     * Structural modifications are those that change the number of mappings in     * the HashMap or otherwise modify its internal structure (e.g.,     * rehash).  This field is used to make iterators on Collection-views of     * the HashMap fail-fast.  (See ConcurrentModificationException).     */    transient int modCount;    // ** some other code, e.g. one example    @SuppressWarnings("unchecked")    @Override    public synchronized void forEach(BiConsumer<? super K, ? super V> action) {        Objects.requireNonNull(action);     // explicit check required in case                                            // table is empty.        final int expectedModCount = modCount;        Entry<?, ?>[] tab = table;        for (Entry<?, ?> entry : tab) {            while (entry != null) {                action.accept((K)entry.key, (V)entry.value);                entry = entry.next;                if (expectedModCount != modCount) {                    throw new ConcurrentModificationException();                }            }        }    }

那就说明,源码中涉及操作存在:modCount++, 有些操作,都做了expectedModCount != modCount 这样的判断,说明HashTable也是支持fail-fast的。(有人说是jdk6做了修改,使其支持fail-fast了,具体的也不清楚)

关于modCount

1、HashTable

首先:我的jdk版本是1.8.0_91

HashTable中,modCount++的地方有:rehash(), addEntry()(Put操作时), remove(), clear(), compute, merger等。HashTable的内部类EntrySet, Enumerator(迭代器)中涉及修改操作HashTable对象的地方,也都同时modCount++了,

clone()方法中将新生成的HashTable对象的:modCount=0

那又如何触发fail-fast呢? 看下面例子:

    @SuppressWarnings("unchecked")    @Override    public synchronized void forEach(BiConsumer<? super K, ? super V> action) {        Objects.requireNonNull(action);     // explicit check required in case                                            // table is empty.        final int expectedModCount = modCount;        Entry<?, ?>[] tab = table;        for (Entry<?, ?> entry : tab) {            while (entry != null) {                action.accept((K)entry.key, (V)entry.value);                entry = entry.next;                if (expectedModCount != modCount) {                    throw new ConcurrentModificationException();                }            }        }    }

在遍历前,先定义一个final值expectedModCount,等于当前、此刻的modCount,而在遍历HashTable时,每次都会检查,此时这个对象的modCount是否等于,遍历操作前的modCount,如果不等于,说明有别的并发线程操作了这个对象,这样就会抛ConcurrentModificationException异常了。

其他需要做fail-fast的地方的实现与上面相同。再看Enumerator迭代器是如何做的。

        /**         * The modCount value that the iterator believes that the backing         * Hashtable should have.  If this expectation is violated, the iterator         * has detected concurrent modification.         */        protected int expectedModCount = modCount;

HashTable 的内部类
private class Enumerator implements Enumeration, Iterator中有一个protected的成员 expectedModCount 等于当前的modCount, 迭代器中对HashTable对象做修改 modCount的值也会自增一,迭代器中遍历时,会判断expectedModCount是否等于modCount。

2、HashMap

HashMap中,modCount++的地方有:putVal(), removeNode(), clear(), compute(), merger(), HashIterator等等。

触发fail-fast的地方:KeySet内部类中的forEach(), Values中的forEach(), EntrySet中的forEach(), HashIterator等等。

原理与上面一样,就是涉及修改操作对象结构的地方,modCount++, 遍历,replace等地方,判断前后是否一致,不一致说明有其他并发线程修改了,抛出ConcurrentModificationException异常。

TreeMap

TreeMap是一个通过红黑树实现的有序key-value集合;
TreeMap继承AbstractMap, 也即实现了Map,它是一个集合;
TreeMap实现了NavigableMap接口,它支持一系列的导航方法;
TreeMap实现了Cloneable接口,它可以被克隆。

TreeMap基于红黑树(Red-Black tree)实现。映射根据其键的自然顺序进行排序,或者根据创建映射时提供的Comparator进行排序,具体取决于使用的构造方法。TreeMap的基本操作containsKey, get, put, remove方法,它的时间复杂度是log(n).

TreeMap是非同步的。

TreeMap本质是红黑树,包含几个重要的成员变量:root, size, comparator。其中root是红黑树的根节点。它是Entry类型,Entry是红黑树的节点,它包含了红黑树的6个基本组成:key, value, left, right, parent和color。Entry节点根据key排序,包含的内容是value。Entry中的key比较大小是根据比较器comparator来进行判断的。size是红黑树的节点个数。

先了解一下什么是红黑树:

红黑树

红黑树 又称 红-黑二叉树,是一棵自平衡的排序二叉树。

平衡二叉树,需要满足:树中任何节点的值大于它的左子节点,且小于它的右子节点。这样使得树的检索效率大大提高。为了维持二叉树的平衡,有很多算法:AVL、SBT、Red-Black Tree、伸展树等(现在看名字我还知道,具体的算法实现好像貌似都还给老师了,AVL还有点印象o(╯□╰)o老师,我对不起你啊!)

平衡二叉树必须具备如下特性: 它是一棵空树 或 它的左右两个子树的高度差的绝对值不超过1, 并且左右两个子树都是一棵平衡二叉树。

红黑树故名思义就是节点是红色或者黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。对于一棵有效的红黑树而言,增加了如下规则:
1. 每个节点都只能是红色或者黑色
2. 根节点是黑色
3. 每个节点(Nil节点,空姐点)是黑色的
4. 如果一个节点是红色的,则它的两个子节点都是黑的。也就是说在一条路径上不能出现相邻的两个红色节点。
5. 从任一节点到每个叶子的所有路径都包含相同数目的黑色节点

插入红黑树图片

TreeMap源码(jdk1.8)

TreeMap定义:

public class TreeMap<K,V>    extends AbstractMap<K,V>    implements NavigableMap<K,V>, Cloneable, java.io.Serializable

TreeMap重要属性的定义:

    /**     * The comparator used to maintain order in this tree map, or     * null if it uses the natural ordering of its keys.     *     * @serial     */    private final Comparator<? super K> comparator;    private transient Entry<K,V> root;    /**     * The number of entries in the tree     */    private transient int size = 0;    /**     * The number of structural modifications to the tree.     */    private transient int modCount = 0;    // Red-black mechanics    private static final boolean RED   = false;    private static final boolean BLACK = true;
  • comparator 比较器,用来给TreeMap排序
  • root 红黑树根节点
  • size 红黑树的节点总数
  • modCount 和上面HastTable HashMap一样,TreeMap的修改次数,用来实现fail-fast的。
  • RED & BLACK 红黑树的颜色

对于叶子节点Entry(root的类型也是Entry

    /**     * Node in the Tree.  Doubles as a means to pass key-value pairs back to     * user (see Map.Entry).     */    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;  // 颜色        /**        * methods        */    }

其他的源码解读参考下面这篇文章:

TreeMap实现原理

Java TreeMap源码解析

TreeMap使用场景

TreeMap就是一个Map, 也是以key-value的形式存储数据。TreeMap通常比HashMap、HashTable要慢(尤其是在插入、删除key-value时更慢),因为TreeMap底层采用红黑树来管理键值对。不过TreeMap的好处是:TreeMap中的key-value对总是处于有序状态,无须专门进行排序操作。并且虽然TreeMap在插入和删除方面性能比较差,但是在分类处理的时候作用很大,遍历的速度很快。

List接口(ArrayList, LinkedList, Vector)

常用的实现了List接口的有:ArrayList, LinkedList, 还有Vector(不常用,没用过)
最常用的当然是ArrayList

public class ArrayList<E> extends AbstractList<E>        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
public class LinkedList<E>    extends AbstractSequentialList<E>    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
public class Vector<E>    extends AbstractList<E>    implements List<E>, RandomAccess, Cloneable, java.io.Serializable

**
ArrayList是基于动态数组的数据结构**

Vertor和ArrayList一样,也是基于动态数组的数据结构,不过它们的区别也很明显。
1. Vertor是强线程安全的,ArrayList是非线程安全的,和(HashTable, HashMap)(StringBuilder, StringBuffer)一样,ArrayList中的所有方向都没有synchronize修饰;而Vertor中的大多数(涉及操作的 几乎)都被synchronize修饰,保证线程安全的。
2. 第二点区别是当它们在add时,内存空间不够了,它们的空间动态增长不同。
ArrayList的增加算法如下:int newCapacity = oldCapacity + (oldCapacity >> 1), 这句话看出,它的增长空间是 原来空间的一半(oldCapacity>>1)

    /**     * Increases the capacity to ensure that it can hold at least the     * number of elements specified by the minimum capacity argument.     *     * @param minCapacity the desired minimum capacity     */    private void grow(int minCapacity) {        // overflow-conscious code        int oldCapacity = elementData.length;        int newCapacity = oldCapacity + (oldCapacity >> 1);// 增加原来50%的空间        if (newCapacity - minCapacity < 0)// 如果容器扩容之后还是不够,就直接设置min            newCapacity = minCapacity;        if (newCapacity - MAX_ARRAY_SIZE > 0)            newCapacity = hugeCapacity(minCapacity);        // minCapacity is usually close to size, so this is a win:        elementData = Arrays.copyOf(elementData, newCapacity);    }

Vertor增长为原来的两倍:int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);

    private void grow(int minCapacity) {        // overflow-conscious code        int oldCapacity = elementData.length;        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?                                         capacityIncrement : oldCapacity);        if (newCapacity - minCapacity < 0)            newCapacity = minCapacity;        if (newCapacity - MAX_ARRAY_SIZE > 0)            newCapacity = hugeCapacity(minCapacity);        elementData = Arrays.copyOf(elementData, newCapacity);    }

注:具体的Vector还有些不同,有一个增长因子,暂时不管。有兴趣的可以看这篇blog

Vector和ArrayList的比较

LinkedList是基于链表的数据结构

这三种数据结构应该是大家最为熟悉的。再次就不多说了。

说了这么多,也都是最基础的关于集合的一些东西(还没有涉及Set)只列举了两种经常使用的List和Map,结合源码,解决了一些曾经知道的知识,但是不知道底层实现的东西(IDEA就是好,都不用装反编译工具,直接出现源码。用这个读源码,兼职不能再方便)

其实,主要想说的还是fail-fast机制以及对应的解决这个问题的解决方案;那么下面重点来了。

三、fail-fast机制

fail-fast机制,在上面已经提到多次,这些集合的实现类中都有一个feild——modCount用来标记这个集合对象的修改次数。
fail-fast它是java集合的一种错误检测机制。某个线程在对Collection进行迭代时,不允许其他线程对该Collection进行结构上的修改。下面以java为例

 * <p><a name="fail-fast"> * The iterators returned by this class's {@link #iterator() iterator} and * {@link #listIterator(int) listIterator} methods are <em>fail-fast</em>:</a> * if the list is structurally modified at any time after the iterator is * created, in any way except through the iterator's own * {@link ListIterator#remove() remove} or * {@link ListIterator#add(Object) add} methods, the iterator will throw a * {@link ConcurrentModificationException}.  Thus, in the face of * concurrent modification, the iterator fails quickly and cleanly, rather * than risking arbitrary, non-deterministic behavior at an undetermined * time in the future. * * <p>Note that the fail-fast behavior of an iterator cannot be guaranteed * as it is, generally speaking, impossible to make any hard guarantees in the * presence of unsynchronized concurrent modification.  Fail-fast iterators * throw {@code ConcurrentModificationException} on a best-effort basis. * Therefore, it would be wrong to write a program that depended on this * exception for its correctness:  <i>the fail-fast behavior of iterators * should be used only to detect bugs.</i>

迭代器的fail-fast行为无法得到保证,一般来说,不可能对是否出现不同步并发修改做出任何硬件保证。fail-fast迭代器会尽最大努力抛出ConcurrentModificationException。因此,在写程序时依赖这一异常来保证迭代器的正确性是错误的:迭代器的快速失败行为应该仅仅用于检测bug。

HashMap的类注释上也有此段类似的注释。

ArrayList中(HashMap等其它有此机制的集合)都有一个field: modCount, 在每次对这个ArrayList这个对象做结构上的修改时,如Add,Remove, Clear什么的,modCount都会自增一。然后在迭代时,迭代前,会
int expectedModCount = modCount (HashMap中是: int mc = modCount ��其实都一样),
然后在每次遍历迭代时,都会比较 expectedModCount是否等于modCount, 如果不等于,就抛出ConcurrentModificationException异常。这就实现了fail-fast机制了。

解决方案

在并发,多线程的情况下,为了避免出现fail-fast导致程序异常。可以使用下面两种方案:

  • 方案一:在遍历的过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。(但是不推荐,增删造成的同步锁可能会阻塞遍历操作)
  • 方案二:使用CopyOnWriteArrayList来替换ArrayList。(推荐)

CopyOnWriteArrayList是ArrayList的一个线程安全的变体。
其中所有可变的操作(add,set)等都是通过对底层数组进行一次新的复制来实现的。所以产生的开销比较大。不过在下面两种情况下很适用
1. 在不能活不想进行同步遍历,但又需要从并发线程中排除冲突时。
2. 当遍历操作的数量大大超过可变操作的数量时。

public class CopyOnWriteArrayList<E>    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {    private static final long serialVersionUID = 8673264195747942595L;    /** The lock protecting all mutators */    final transient ReentrantLock lock = new ReentrantLock();    // .......}

补充说明一下:
1. CopyOnWriteArrayList在数据结构、定义都和ArrayList一样。都是事先List接口,底层使用动态数组实现,方法上也一样。
2. CopyOnWriteArrayList不会产生ConcurrentModificationException异常。压根没有modCount这个东西。

CopyOnWriteArrayList解决fail-fast原理(以add()方法为例):

ArrayList的add()方法:

    /**     * Appends the specified element to the end of this list.     *     * @param e element to be appended to this list     * @return <tt>true</tt> (as specified by {@link Collection#add})     */    public boolean add(E e) {        ensureCapacityInternal(size + 1);  // Increments modCount!!        elementData[size++] = e;        return true;    }    private void ensureCapacityInternal(int minCapacity) {        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);        }        ensureExplicitCapacity(minCapacity);    }    private void ensureExplicitCapacity(int minCapacity) {        modCount++;        // overflow-conscious code        if (minCapacity - elementData.length > 0)            grow(minCapacity);    }

简单直接的在数组后面增加了一个元素(增加之前检查了空间是否足够,不够扩容,并modCount++)

CopyOnWriteArrayList的add()方法:

    /**     * Appends the specified element to the end of this list.     *     * @param e element to be appended to this list     * @return {@code true} (as specified by {@link Collection#add})     */    public boolean add(E e) {        final ReentrantLock lock = this.lock;        lock.lock();        try {            Object[] elements = getArray();            int len = elements.length;            Object[] newElements = Arrays.copyOf(elements, len + 1);            newElements[len] = e;            setArray(newElements);            return true;        } finally {            lock.unlock();        }    }    final Object[] getArray() {        return array;    }    final void setArray(Object[] a) {        array = a;    }

首先,没有modCount这个东西。然后很明显这个的实现复杂了很多,又是加锁,又是复制复制原来的对象,然后修改操作都是在新复制的新对象上操作的,这样其他的并发操作这个ArrayList的时候,就不会出现冲突了。然后add完之后,再将这个新的数组塞回去。(代码清晰、明显,一看就懂,就不多废话了)

CopyOnWriteArrayList果然“人如其名”,先Copy,然后操作,然后再给Write回去,在这个add()的上层完全感觉不到。

参考

ConcurrentHashMap

Java集合——ConcurrentHashMap

ConcurrentHashMap的实现原理

Java并发编程之ConcurrentHashMap

聊聊并发(四)——深入分析ConcurrentHashMap

探索jdk8之ConcurrentHashMap 的实现机制

有些复杂,目前只是大概了解了,所以还是先贴上几个 我看个博客吧。

注:ConcurrentHashMap中扩容等操作用到了CAS。
so…..

CAS(Compare And Swap)

CAS是现代CPU广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子性读写操作。

CAS包含3个操作数:需要读写的内存位置V,进行比较的值A,拟写入的新值B。

当且仅当V的值等于A时,CAS才会通过原子方式用新值B来更新V的值,否则不会执行任何操作。无论位置V的值是否等于A,都将返回V原有的值。(这种变化被称之为比较并设置,无论操作是否成功都会返回)。CAS的含义是:“我认为V的值应该是A,如果是,那么将V的值更新为B,否则不修改并告诉V的实际值为多少”。CAS是一项乐观的技术,它希望能成功地执行更新操作,并且如果有另外一个线程在最近一次检查后更新了该变量,那么CAS能检测到这个错误。(摘自:《Java并发编程实战》 $15.2)

上面那个V, A, B理解起来有点绕。简单的说就是:V是该值在内存中的位置,A是原来的旧值,B是需修改的新值。

e.g.

public class SimulatedCAS {    @GuardedBy("this")    private int value;    public synchronized int get() {        return value;    }    public synchronized int compareAndSwap(int expectedValue, int newValue) {        int oldValue = value;        if (oldValue == expectedValue) {            value = newValue;        }        return value;    }    public synchronized boolean compareAndSet(int expectedValue, int newValue) {        return (expectedValue == compareAndSwap(expectedValue, newValue));    }}

value即为 V,expectedValue即为A,newValue即为B

还有Comapare And Set, Copy And Set等。都叫CAS- - ��

0 0