Collections 浅析(一)

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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值