1.7源码阅读-Java集合

Java集合提供了存储数据和对象的类,其主要的关系如下图。

集合类关系图.gif

CollectionMap为其中两个主要的接口,定义了两种存储数据的方式。Collection直接存储数据,而Map通过键值对来存储数据。下面是这两个接口定义的方法。

public interface Collection<E> extends Iterable<E> {
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    Iterator<E> iterator();
    Object[] toArray();
    <T> T[] toArray(T[] a);
    boolean add(E e);
    boolean remove(Object o);
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
    boolean removeAll(Collection<?> c);
    //保留集合C中的元素,删除其他元素
    boolean retainAll(Collection<?> c);
    void clear();
    boolean equals(Object o);
    int hashCode();
}
public interface Map<K,V> {
    int size();
    boolean isEmpty();
    boolean containsKey(Object key);
    boolean containsValue(Object value);
    V get(Object key);
    V put(K key, V value);
    V remove(Object key);
    void putAll(Map<? extends K, ? extends V> m);
    void clear();
    Set<K> keySet();
    Collection<V> values();
    Set<Map.Entry<K, V>> entrySet();
    boolean equals(Object o);
    int hashCode();
    interface Entry<K,V> {
        K getKey();
        V getValue();  
        V setValue(V value);
        boolean equals(Object o);
        int hashCode();
    }
}

不难发现CollectionMap定义的方法十分相似,其中主要不同的有Map中定义了一个键值对的接口Entry<K,V>,以及Map不支持IteratortoArray()方法。

1 List

List继承了Collection的接口,除了Collection定义的方法外,List接口中新增了下面的这些方法。Collection中已定义了Iterator,而List中增加了ListIterator,在原Iterator的基础上增加了向前遍历、增加的方法,更加实用,可以说是能用ListIterator就不用Iterator

public interface List<E> extends Collection<E> {
    //Collection中已实现的方法不再赘述

    E get(int index);
    E set(int index, E element);
    void add(int index, E element);
    E remove(int index);
    int indexOf(Object o);
    int lastIndexOf(Object o);
    ListIterator<E> listIterator();
    ListIterator<E> listIterator(int index);
    List<E> subList(int fromIndex, int toIndex);
}

AbstractList类实现继承了AbstractCollection类,实现了List接口,其中AbstractCollection类实现了Collection中定义的部分方法。
AbstractList除了实现了List中定义的一些方法外,更主要的是具体实现了IteratorListIterator这两个私有类和SubList快照。

AbstractList的实现类有ArrayListLinkedListVector,其特点是有序,可重复。

ArrayList

ArrayList继承了AbstractList,实现了ListRandomAccessCloneablejava.io.Serializable接口。。
ArrayList使用数组对数据进行存储,定义了EMPTY_ELEMENTDATA

private static final Object[] EMPTY_ELEMENTDATA = {};

当创建了ArrayList对象且还未存入任何数据时,存储数据的数组Object[] elementData还未实现,只有第一次存入数据时才真正的为该数组申请内存空间。
ArrayList重写了AbstractList中的方法,实现方式并不复杂,其中比较值得学习的一点是源码中将重复的简短代码也进行了函数包装,使得阅读代码非常容易。

private void rangeCheck(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

此外,ArrayList实现了对数elementData数组进行扩充的方法,当数组容量不够时,会将容量增加到原来的1.5倍并将原数组中的元素拷贝到新数组中。

ArrayList重新实现了快照,即私有类SubList,可以通过subList(fromIndex,toInex)获得。该私有类实现了ArrayList的所有方法,并且在该对象引用上的操作会直接影响原ArrayList。当获得了一个ArrayList的快照时,不能修改原ArrayList,否则会产生fast-fail错误。(详见http://cmsblogs.com/?p=1220
该错误主要目的是为了防止多个线程通过迭代器操作同一ArrayList时,一个线程对其修改导致了另一线程的操作产生不符合目的的结果。其实现的机制是在ArrayList中存在 modCount变量,存储该ArrayList被修改容量的次数。当通过IteratorSubListArrayList进行修改时,判断modCount是否复合预期值(值在操作该ArrayList前将其保存)。

Vector

VectorArrayList的实现方式基本相同,都是通过数组来实现,其主要的区别在于Vector线程同步的,即多个线程操作同一个Vector也是安全的。Vector通过在add()remove()等对Vector进行修改的方法上使用synchronized进行修饰。因此在大多数情况下,能使用ArrayList就要避免使用Vector,以此来避免系统为了保证线程同步而产生的性能消耗。

LinkedList

LinkedList继承了AbstractSequentialList,实现了ListDequeCloneablejava.io.Serializable接口。
AbstractSequentialList继承了AbstractList,但并未增加任何新方法。
LinkedListArrayList的主要区别是其使用了链表来存储数据,以及实现了Deque接口,Deque接口中定义的peek()pop()push()等方法,可以将其当作栈使用,其栈顶为链表首部,栈底为链表尾部。LinkedListadd()remove()contains()等方法的实现方式与ArrayList区别不大。

private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
}

2 Set

Set接口继承了Collection接口,但并没有实现或增加任何功能。
AbstractSet继承了AbstractCollection类,实现了Set接口。
AbstractSet主要有HashSetTreeSet两个子类,其基本实现分别基于HashMapTreeMap

HashSet

HashSet在类内创建了一个HashMap和一个Object对象。

private transient HashMap<E,Object> map;
private static final Object PRESENT = new Object();

对于需要放入HashSet的元素,以该元素和PRESENT构建键-值对,存入map中。其中该元素为Key,这样就保证了HashSet中不存在相同的元素,并且可以通过调用HashMapadd()remove()等方法直接实现其自身的方法。

TreeSet

TreeSet继承了AbstractSet,实现了NavigableSetCloneablejava.io.Serializable接口。
SortedSet是一个有序集合的接口,任何存入实现了该接口的类的对象需要实现Comparable接口。
NavigableSet接口继承了SortedSet接口,实现了元素的比较等方法。

public interface NavigableSet<E> extends SortedSet<E> {
    //返回比e小的最大元素
    E lower(E e);
    E higher(E e);
    //返回不比e大的最大元素
    E floor(E e);
    E ceiling(E e);
    //删除并返回最小元素
    E pollFirst();
    E pollLast();
    Iterator<E> iterator();
    NavigableSet<E> descendingSet();
    Iterator<E> descendingIterator();
    //返回视图
    NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                           E toElement,   boolean toInclusive);
    //返回比toElement小的元素(或等于,若inclusive==True)
    NavigableSet<E> headSet(E toElement, boolean inclusive);
    NavigableSet<E> tailSet(E fromElement, boolean inclusive);
    SortedSet<E> subSet(E fromElement, E toElement);
    SortedSet<E> headSet(E toElement);
    SortedSet<E> tailSet(E fromElement);
}

TreeSet存储数据的方式与HashSet类似,TreeSet默认创建了TreeMap来存储数据,利用键值对保证数据的唯一性。

3 Queue

Queue接口继承了Collection接口,定义了如下方法。

public interface Queue<E> extends Collection<E> {
    boolean add(E e);
    //插入元素
    boolean offer(E e);
    E remove();
    //返回并删除队首
    E poll();
    //返回队首,若不存在则抛出异常
    E element();
    //返回队首,若不存在则返回null
    E peek();
}

这里主要介绍PriorityQueueConcurrentLinkedQueue

  • PriorityQueue
    PriorityQueue继承了AbstractQueue,实现了java.io.Serializable接口。
    优先队列使用平衡二叉堆保证了非严格有序地存储数据(仅保证父亲小于儿子),默认的顺序是对象从小到大的顺序,也可以通过自定义比较器来设定数据排序的顺序。
  1. 存储
    PriorityQueue使用数组对平衡二叉堆进行存储,queue[n]的两个儿子分别是queue[2*n+1]queue[2*(n+1)]queue[0]保存了最小的元素。默认的数组大小为11,当数组不够用时,会根据当前数组的大小来扩大。如果当前数组小于64则容量翻倍,否则增加至1.5倍。
    private transient Object[] queue;
    
  2. 修改
    当从优先队列删除或增加一个元素时,为了保证堆的性质,需要对元素进行上移或下移。
    当增加一个元素时,将该元素插入到末端,然后将其不断上移,直到其父亲的值大于其值;当删除一个元素时,将队列末端的元素插入到该元素的位置,然后通过上移或下移该元素的位置,使得堆仍保持性质。
  • ConcurrentLinkedQueue
    ConcurrentLinkedQueue在类内创建了Node类,该类使用了Unsafe类的compareAndSwap方法实现了线程同步。Node类还使用了和ConcurrentHashMap相同的方法,通过Unsafe类和偏移直接修改JVM。
    private static class Node<E> {
        volatile E item;
        volatile Node<E> next;
    
        Node(E item) ;
    
        boolean casItem(E cmp, E val) {
            return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
        }
    
        void lazySetNext(Node<E> val) {
            UNSAFE.putOrderedObject(this, nextOffset, val);
        }
    
        boolean casNext(Node<E> cmp, Node<E> val) {
            return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
        }
    
        // Unsafe mechanics
        private static final sun.misc.Unsafe UNSAFE;
        private static final long itemOffset;
        private static final long nextOffset;
    
        static {
            try {
                UNSAFE = sun.misc.Unsafe.getUnsafe();
                Class k = Node.class;
                itemOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("item"));
                nextOffset = UNSAFE.objectFieldOffset
                    (k.getDeclaredField("next"));
            } catch (Exception e) {
                throw new Error(e);
            }
        }
    }
    
  1. 增加
    ConcurrentLinkedQueue通过offer()实现了对元素的增加。每次增加时并不立即修改tail,而是当每2次增加后才修改,知道这点后,阅读代码就方便了许多。
    public boolean offer(E e) {
        checkNotNull(e);
        final Node<E> newNode = new Node<E>(e);
    
        for (Node<E> t = tail, p = t;;) {
            Node<E> q = p.next;
            if (q == null) {
                // p is last node
                if (p.casNext(null, newNode)) {
                    // 如果是第一次添加的线程,则直接返回
                    // 如果是第二次添加的线程,更改tail
                    if (p != t) // hop two nodes at a time
                        casTail(t, newNode);  // Failure is OK.如果失败则说明其他线程已经更改了tail
                    return true;
                }
                // Lost CAS race to another thread; re-read next
            }
            //如果p==q,说明该结点已经不在队列中,需要重新从头遍历
            else if (p == q)
                p = (t != (t = tail)) ? t : head;
            else
                // Check for tail updates after two hops.
                p = (p != t && t != (t = tail)) ? t : q;
        }
    }
    
  2. 删除
    poll()方法实现了从队首移除一个元素。和offer()方法一样,每当移除2个元素后才会更改head
    public E poll() {
        restartFromHead:
        for (;;) {
            for (Node<E> h = head, p = h, q;;) {
                E item = p.item;
    
                if (item != null && p.casItem(item, null)) {
                    // 第二个移除元素的线程更改头部
                    if (p != h) // hop two nodes at a time
                        updateHead(h, ((q = p.next) != null) ? q : p);
                    return item;
                }
                //如果整个队列为空
                else if ((q = p.next) == null) {
                    updateHead(h, p);
                    return null;
                }
                //如果p结点已经从队列移除,则重新遍历
                else if (p == q)
                    continue restartFromHead;
                else
                    p = q;
            }
        }
    }
    

4 Map

最常用的两类MapHashMapTreeMap

HashMap

HashMap继承了AbstractMap,实现了MapCloneablejava.io.Serializable
HashMap类中实现了一个Entry<K,V>类来保存每一个键-值对。HashMap通过Entry[]数组来保存所有的键-值对,其中每一个索引称为一个桶(bucket)。

  • 键-值对的存储
    Entry[]数组可能包含多个桶,那如何确定某个键-值对应该保存在哪个桶中呢?Java中通过对键取hash并对数组长度取余,将该键-值对存放到余数对应的桶中。
final int hash(Object k) {
        int h = hashSeed;
        if (0 != h && k instanceof String) {
            return sun.misc.Hashing.stringHash32((String) k);
        }

        h ^= k.hashCode();

        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
}

最后两行代码被称为扰动函数,这两行代码对原hash值进行了4次异或处理,使每一位与不同的4位进行异或。当原Object的hashCode()方法实现不佳,不同Object对象hashCode()方法的返回值可能会呈等差数列等冲突较多的情况时,使用扰动函数能够将降低冲突,即降低了不同Object对象保存在同一个桶中的可能性。

static int indexFor(int h, int length) {
        // assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
        return h & (length-1);
}

该方法计算了Object的hash对应桶的索引。HashMapEntry[]数组长度要求是2的指数,这样length-1就是一个低位的掩码,和h进行与操作后,得到的就是余数,即对应的桶。

  • 桶的扩充
    当往一个HashMap中存入远超其桶数量的键-值对时,冲突的次数会大幅度增加,因此需要根据数据的多少来动态更改桶的个数。
    在初始化HashMap时需要输入负载因子(loadFactor),每当HashMap中的键-值对总数量大于桶个数X负载因子时,HashMap便会使容量X2,并重新放置原键-值对。

HashMap提供了和ListIterator类似的HashIterator,该类是抽象类,其实现类有ValueIteratorKeyIteratorEntryIterator。这几个类均实现了从首桶到尾桶遍历所有元素。
HashMap也提供了和SubList类似的KeySetValuesEntrySet视图来供使用。

TreeMap

TreeMap继承了AbstractMap,实现了NavigableMapCloneablejava.io.Serializable接口。
TreeMapHashMap的主要不同之处是使用了红黑树存储键值对。

ConcurrentHashMap

ConcurrentHashMap继承了AbstractMap,实现了ConcurrentMapSerializable接口。

public interface ConcurrentMap<K, V> extends Map<K, V> {
    //如果存在key,则返回对应的value;否则放入key-value
    V putIfAbsent(K key, V value);
    boolean remove(Object key, Object value);
    boolean replace(K key, V oldValue, V newValue);
    //如果key存在,则将value替换
    V replace(K key, V value);
}
  • Segment
    ConcurrentHashMap实现了Segment<K,V>类来代替HashMap中的elementData数组,以实现多线程的并发访问。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
    //以下是Segment实现的方法,申明的变量以及各个方法的具体实现未拷贝
    Segment(float lf, int threshold, HashEntry<K,V>[] tab) ;
    final V put(K key, int hash, V value, boolean onlyIfAbsent) ;
    private void rehash(HashEntry<K,V> node);
    private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value);
    private void scanAndLock(Object key, int hash);
    final V remove(Object key, int hash, Object value);
    final boolean replace(K key, int hash, V oldValue, V newValue);
    final V replace(K key, int hash, V value);
    final void clear();
}

Segment<K,V>类中对外的方法均是线程安全的,使用了可重入锁以保证这一特性。以put()为例。

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        //put操作
    } finally {
        unlock();
    }
    return oldValue;
}

在该方法中先使用了tryLock()尝试直接获得锁。如果获得锁,则直接进行操作;否则再调用scanAndLockForPut()方法。

private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
    //得到存储数据的入口
    HashEntry<K,V> first = entryForHash(this, hash);
    HashEntry<K,V> e = first;
    HashEntry<K,V> node = null;
    int retries = -1; // negative while locating node
    while (!tryLock()) {
        HashEntry<K,V> f; // to recheck first below
        //如果重试次数<0
        if (retries < 0) {
            if (e == null) {
                if (node == null) // speculatively create node
                        node = new HashEntry<K,V>(hash, key, value, null);
                retries = 0;
            }
            else if (key.equals(e.key))
                retries = 0;
            else
                e = e.next;
            }
        //如果重试次数超过了上限时(如果jvm允许多线程则为64,否则为1)
        else if (++retries > MAX_SCAN_RETRIES) {
            lock();
            break;
        }
        else if ((retries & 1) == 0 &&
            (f = entryForHash(this, hash)) != first) {
                e = first = f; // re-traverse if entry changed
                retries = -1;
            }
        }
        return node;
}

该方法实现了在不断调用tryLock()的同时遍历该Segment对象中存储的数据,如果遍历一遍后发现不存在该键值对,则创建node并初始化,等到获取锁后再返回。在put()中,获得锁后先遍历一次,若该键值对没有被其他线程添加,则在表中加入node,这样就避免了添加key相同的键值对。这种方法让线程不是傻等锁的释放,而让线程在不断调用tryLock()的同时还遍历了数据,更大程度的榨取了线程的剩余价值。

  • ConcurrentHashMap的并发度
    在初始化ConcurrentHashMap对象时可以设定并发程度,并发程度越高,创建的Segment对象越多,对hash值区段的划分越细,直接使得不同线程访问相同Segment对象的冲突减少。
    默认的concurrencyLevel为16,即创建16个Segment对象。
public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel);
  • Unsafe类
    ConcurrentHashMap中使用了Unsafe类,Unsafe类可以直接观察并操作JVM。
 private Segment<K,V> ensureSegment(int k) {
    final Segment<K,V>[] ss = this.segments;
    long u = (k << SSHIFT) + SBASE; // raw offset
    Segment<K,V> seg;
    if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
        //do somthing
    }
    return seg;
}

像在ensureSegment()方法中,先计算了目标Segment对象的偏移,然后利用Unsafe.getObjectVolatile()获得了该对象的引用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 在一个方法内部定义的变量都存储在栈中,当这个函数运行结束后,其对应的栈就会被回收,此时,在其方法体中定义的变量将不...
    Y了个J阅读 4,413评论 1 14
  • Java源码研究之容器(1) 如何看源码 很多时候我们看源码, 看完了以后经常也没啥收获, 有些地方看得懂, 有些...
    骆驼骑士阅读 981评论 0 22
  • Collection接口 Collection接口是所有集合的祖先类。他有两个构造方法,一个无参构造,一个是带Co...
    夜幕繁华阅读 583评论 0 0
  • ArrayList实现原理要点概括 参考文献:http://zhangshixi.iteye.com/blog/6...
    晨光光阅读 1,061评论 0 1
  • 尽信书,不如无书。可是我宁愿相信历史的真相是这样的,这摧毁了我对秦史的认知。对秦始皇,李斯,韩非重新从头到脚...
    小猪天堂阅读 528评论 0 2