LinkedList, CopyOnWriteArrayList 1.8原理分析

目录

目录.png

LinkedList

继承关系

  • LinkedList 实现 Deque (继承自Queue接口)接口,即能将LinkedList当作双端队列使用。存储元素过程中,无需像 ArrayList 那样进行扩容。但有得必有失,LinkedList 存储元素的节点需要额外的空间存储前驱和后继的引用。LinkedList 在链表头部和尾部插入效率比较高,但在指定位置进行插入时,效率一般(此操作的时间复杂度为O(N))。
  • LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆,这里是浅复制。
  • Serializable接口表示可序列化
  • LinkedList相对于ArrayList来说,是可以快速添加,删除元素,ArrayList(非尾部添加)添加删除元素的话需移动数组元素,可能还需要考虑到扩容数组长度
// 可以用此类替换Stack
Queue queue = new LinkedList<>();
public class LinkedList
    extends AbstractSequentialList
    implements List, Deque, Cloneable, java.io.Serializable

add方法

  • 新增的元素放置链表的最后面,然后链表的长度(size)加1,修改的次数(modCount)加1
/** 在链表尾部插入元素 */
public boolean add(E e) {
    linkLast(e);
    return true;
}

/** 在链表指定位置插入元素 */
public void add(int index, E element) {
    checkPositionIndex(index);

    // 判断 index 是不是链表尾部位置,如果是,直接将元素节点插入链表尾部即可
    if (index == size)
        linkLast(element);
    else
        linkBefore(element, node(index));
}

/** 将元素节点插入到链表尾部 */
void linkLast(E e) {
    final Node l = last;
    // 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空
    final Node newNode = new Node<>(l, e, null);
    // 将 last 引用指向新节点
    last = newNode;
    // 判断尾节点是否为空,为空表示当前链表还没有节点
    if (l == null)
        first = newNode;
    else
        l.next = newNode;    // 让原尾节点后继引用 next 指向新的尾节点
    size++;
    modCount++;
}

/** 将元素节点插入到 succ 之前的位置 */
void linkBefore(E e, Node succ) {
    // assert succ != null;
    final Node pred = succ.prev;
    // 1. 初始化节点,并指明前驱和后继节点
    final Node newNode = new Node<>(pred, e, succ);
    // 2. 将 succ 节点前驱引用 prev 指向新节点
    succ.prev = newNode;
    // 判断尾节点是否为空,为空表示当前链表还没有节点    
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;   // 3. succ 节点前驱的后继引用指向新节点
    size++;
    modCount++;
}

get方法

  • LinkedList 查找过程要稍麻烦一些,需要从链表头结点(或尾节点)向后查找,时间复杂度为 O(N)
public E get(int index) {
    checkElementIndex(index);
    return node(index).item;
}

Node node(int index) {
    // 根据索引位置,决定是前向遍历,还是后向遍历
    if (index < (size >> 1)) {
        Node x = first;
        for (int i = 0; i < index; i++)
            x = x.next;
        return x;
    } else {
        Node x = last;
        for (int i = size - 1; i > index; i--)
            x = x.prev;
        return x;
    }
}

CopyOnWriteArrayList

简介

  • 适合只读操作远多于可变操作,多线程安全。通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove() 等等)的开销很大,这样做的目的是读写分离,读取的时候不会改变读取的内容,写入完成后下次读取就能读取到新内容,所以读不用加锁。
  • 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突(因为读的是快照指向原数组,写的时候是copy然后指向新数组)。在构造迭代器时,迭代器依赖于不变的数组快照。
  • CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先将原有数组的元素拷贝到新数组中,然后在新的数组中做写操作,写完之后,再将原来的数组引用(volatile 修饰的数组引用)指向新数组。CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的

迭代

  • snapshot的目的是,当同时有线程读和写时,写的操作会复制数组A到新数组B,写入,然后将之前的数组A指向新数组B(虚拟机层面阻塞的,而且速度非常快),迭代的snapshot是指向旧数组A(读取和写入同时,读取时写入未完成所以读取的是A)。
public Iterator iterator() {
    return new COWIterator(getArray(), 0);
}

static final class COWIterator implements ListIterator {
    private final Object[] snapshot;
    private int cursor;

    private COWIterator(Object[] elements, int initialCursor) {
        cursor = initialCursor;
        snapshot = elements;
    }

    public boolean hasNext() {
        return cursor < snapshot.length;
    }

    public boolean hasPrevious() {
        return cursor > 0;
    }

    // 省略部分代码
    // 对于remove(), set(), add()等操作,COWIterator都会抛出异常
}

get操作

  • 返回volatile数组中的第index个元素
public E get(int index) {
    return get(getArray(), index);
}

// volatile
final Object[] getArray() {
    return array;
}

private E get(Object[] a, int index) {
    return (E) a[index];
}

add操作

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    // lock
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 新建一个数组newElements,并将原始数据拷贝到newElements中, newElements数组的长度=“原始数组的长度”+1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        // 将”volatile数组“引用指向newElements数组,这样旧数组就被GC回收了
        setArray(newElements);
        return true;
    } finally {
        // unlock
        lock.unlock();
    }
}

remove

public E remove(int index) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        if (numMoved == 0)
            // 删除最后个元素
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            // 复制数组的时候,不复制要删除的数组
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

创建

// 空的
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

// 直接copy Arrays.copyOf底层System.arraycopy,对数组而已深拷贝,但是对元素而言浅拷贝
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

Collections.sort

  • 一般做法是List XX中实现Comparable
// 参数接收list
public static > void sort(List list) {
    list.sort(null);
}

// ArrayList 和COW list都是调用Arrays.sort, 最终大概率是调用折半插入排序,最坏时O(n平方)
public static  void sort(T[] a, int fromIndex, int toIndex,
                            Comparator c) {
    if (c == null) {
        sort(a, fromIndex, toIndex);
    } else {
        // 省略
    }
}

参考文章

  • LinkedList源码分析(基于JDK8)
  • LinkedList 源码分析(JDK 1.8)
  • CopyOnWriteArrayList源码解析——JDK1.8
  • JAVA的copyonwriteArrayList为什么在写入的时候要重新复制一下数组?

你可能感兴趣的:(LinkedList, CopyOnWriteArrayList 1.8原理分析)