Java容器的源码分析

目录

 ArrayList

Vector

CopyOnWriteArrayList

LinkedList

HashMAp

ConCurrentHashMap

LinkedHashMap

WeakHashMap

ConcurrentCache


扫描二维码关注公众号,回复: 8814418 查看本文章

基于JDK1.8

 ArrayList

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

1.基于数组实现、支持快速随机访问

2.数组默认的大小是10。

添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容

新容量是旧容量的1.5倍。

最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。

3.删除元素的代价很高O(N),需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上。

4.序列化

保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。

ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。

序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。

ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);

5.Fail-Fast

modCount 用来记录 ArrayList 结构发生变化(添加或者删除至少一个元素/调整内部数组的大小)的次数。

在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException

Vector

1.同步:synchronized 进行同步

2.扩容:Vector 的构造函数可以传入 capacityIncrement 参数,是原来的2倍。

3.与ArrayList的比较

  • Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
  • Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍

4. 替代方案

可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。

List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);

也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList

1.读写分离

写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。

写操作需要加锁,防止并发写入时导致写入数据丢失。

写操作结束之后需要把原始数组指向新的复制数组。

2.适应场景

  • 适合读多写少的场景
  • 不适合内存敏感以及对实时性要求很高的场景
    • 内存占用:原来的2倍。
    • 数据不一致:部分写操作的数据还未同步到读数组中。

LinkedList

1.概览

基于双向链表实现,使用 Node 存储链表节点信息。

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

每个链表存储了 first 和 last 指针

2.与ArrayList的比较

ArrayList 基于动态数组实现,LinkedList 基于双向链表实现。ArrayList 和 LinkedList 的区别可以归结为数组和链表的区别:

  • 数组支持随机访问,但插入删除的代价很高,需要移动大量元素;
  • 链表不支持随机访问,但插入删除只需要改变指针。

HashMAp

1.存储结构

内部包含了一个 Entry 类型的数组 table

数组中的每个位置被当成一个桶,一个桶存放一个链表。

HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值和散列桶取模运算结果相同的 Entry。

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

    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

    public final K getKey() {
        return key;
    }

    public final V getValue() {
        return value;
    }

    public final V setValue(V newValue) {
        V oldValue = value;
        value = newValue;
        return oldValue;
    }

    public final boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry e = (Map.Entry)o;
        Object k1 = getKey();
        Object k2 = e.getKey();
        if (k1 == k2 || (k1 != null && k1.equals(k2))) {
            Object v1 = getValue();
            Object v2 = e.getValue();
            if (v1 == v2 || (v1 != null && v1.equals(v2)))
                return true;
        }
        return false;
    }

    public final int hashCode() {
        return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
    }

    public final String toString() {
        return getKey() + "=" + getValue();
    }
}

2.拉链法工作原理:使用除留余数法得到所在的桶下标

新建一个HashMap,默认大小是16。

到链表的插入是以头插法方式进行的

3.put操作

HashMap 允许插入键为 null 的键值对:无法调用 null 的 hashCode() 方法

HashMap 使用第 0 个桶存放键为 null 的键值对

4.确定桶下标

很多操作都需要先确定一个键值对所在的桶下标。

(1)计算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();

    // This function ensures that hashCodes that differ only by
    // constant multiples at each bit position have a bounded
    // number of collisions (approximately 8 at default load factor).
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

(2)取模

令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质:

x   : 00010000
x-1 : 00001111

令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数:

y       : 10110010
x-1     : 00001111
y&(x-1) : 00000010

这个性质和 y 对 x 取模效果是一样的:

位运算的代价比求模运算小的多,因此在进行这种计算时用位运算的话能带来更高的性能。

(3)hash%capacity  :hash值%容量

5.扩容

扩容使用 resize() 实现,需要注意的是,扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中

6.扩容-重新计算桶下标

在进行扩容时,需要把键值对重新计算桶下标,从而放到对应的桶上。在前面提到,HashMap 使用 hash%capacity 来确定桶下标。HashMap capacity 为 2 的 n 次方这一特点能够极大降低重新计算桶下标操作的复杂度。

7.计算数组容量:会自动将传入的容量转化为2的n次方

8.链表转红黑树:桶存储的链表长度大于等于 8 时会将链表转换为红黑树。

9.与Hashtable的比较

  • Hashtable 使用 synchronized 来进行同步。
  • HashMap 可以插入键为 null 的 Entry。
  • HashMap 的迭代器是 fail-fast 迭代器。
  • HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。

ConCurrentHashMap

1.存储结构

ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。

默认的并发级别是16,也就是说可以默认创建16个Segment

2.size操作

每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数

3.改动

JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发度与 Segment 数量相等。

JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。

并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。

LinkedHashMap

1.存储结构

继承自HashMap。

内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。

accessOrder 决定了顺序,默认为 false,此时维护的是插入顺序。

//用于维护顺序的函数
//afterNodeAccess:当一个节点被访问时,如果 accessOrder 为 true,则会将该节点移到链表尾部
//链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。
void afterNodeAccess(Node<K,V> p) { }

//fterNodeInsertion:返回 true 时会移除最晚的节点,也就是链表首部节点 first。
//evict 只有在构建 Map 的时候才为 false,在这里为 true。
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);
    }
}

2.LRU缓存

class LRUCache<K, V> extends LinkedHashMap<K, V> {
//设定最大缓存空间 MAX_ENTRIES 为 3
    private static final int MAX_ENTRIES = 3;

//覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > MAX_ENTRIES;
    }
//使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;
    LRUCache() {
        super(MAX_ENTRIES, 0.75f, true);
    }
}


public static void main(String[] args) {
    LRUCache<Integer, String> cache = new LRUCache<>();
    cache.put(1, "a");
    cache.put(2, "b");
    cache.put(3, "c");
    cache.get(1);
    cache.put(4, "d");
    System.out.println(cache.keySet());
}

//结果:通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。
[3, 1, 4]

WeakHashMap

主要用来实现缓存,由JVM对这部分缓存进行回收

private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>

ConcurrentCache

实现了WeakHashMap的缓存功能

public final class ConcurrentCache<K, V> {

    private final int size;

//经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收
    private final Map<K, V> eden;
//不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。
    private final Map<K, V> longterm;

    public ConcurrentCache(int size) {
        this.size = size;
        this.eden = new ConcurrentHashMap<>(size);
        this.longterm = new WeakHashMap<>(size);
    }
//当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm //获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收。
    public V get(K k) {
        V v = this.eden.get(k);
        if (v == null) {
            v = this.longterm.get(k);
            if (v != null)
                this.eden.put(k, v);
        }
        return v;
    }
//当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm //中,利用虚拟机回收掉一部分不经常使用的对象
    public void put(K k, V v) {
        if (this.eden.size() >= size) {
            this.longterm.putAll(this.eden);
            this.eden.clear();
        }
        this.eden.put(k, v);
    }
}
发布了13 篇原创文章 · 获赞 1 · 访问量 255

猜你喜欢

转载自blog.csdn.net/trytrylmt/article/details/104070314
今日推荐