LinkedList和CopyOnWriteArrayList的知识整理

LinkedList 

1:LinkedList中te实现的接口和ArrayList相比有一个特殊的接口:Deque是一个双端队列,所以LinkList可以作为队列(FIFO)也可以作为栈(LIFO)使用。

  

 

java中队列的实现就是LinkedList: 我们之所以说LinkedList 为双端链表,是因为他实现了Deque 接口;我们知道,队列是先进先出的,添加元素只能从队尾添加,删除元素只能从队头删除,Queue中的方法就体现了这种特性。 支持队列的一些操作,我们来看一下有哪些方法实现:

  • pop()是栈结构的实现类的方法,返回的是栈顶元素,并且将栈顶元素删除
  • poll()是队列的数据结构,获取对头元素并且删除队头元素
  • push()是栈结构的实现类的方法,把元素压入到栈中
  • peek()获取队头元素 ,但是不删除队列的头元素
  • offer()添加队尾元素

可以看到Deque 中提供的方法主要有上述的几个方法,接下来我们来看看在LinkedList 中是如何实现这些方法的。

 

 2:向指定位置插入数据时候会先得到指定位置的前一个元素,这时候会用到“二分”的查找,效率比直接查找高一些。

 

 3:迭代器,LinkList中的迭代器比ArrayList中特殊一些。

     1):支持向前遍历也支持向后遍历。

      2): 可以指定迭代开始的位置,不同从第一个元素开始迭代

      3)迭代过程中可以修改元素,甚至可以添加元素删除元素。

CopyOnWriteArrayList

我们都知道ArrayList虽然是很方便的集合,但是它不是线程安全的,为了提供线程安全的集合,在jdk中提供了 Collections.synchronizedxxx

可以把线程不安全的集合转成线程安全的集合,但是这种得到的集合用起来和Vector差不了多少,因为它转换之后的集合把对元素操作的方法都加了锁。

 

对于线程安全的集合还有一个:CopyOnWriteArrayList里处理写操作(包括add、remove、set等)是先将原始的数据通过JDK1.6的Arrays.copyof()来生成一份新的数组,然后在新的数据对象上进行写,写完后再将原来的引用指向到当前这个数据对象,这样保证了每次写都是在新的对象上(因为要保证写的一致性,这里要对各种写操作要加一把锁,JDK1.6在这里用了重入锁),然后读的时候就是在引用的当前对象上进行读(包括get,iterator等),不存在加锁和阻塞,针对iterator使用了一个叫COWIterator的阉割版迭代器,因为不支持写操作,当获取CopyOnWriteArrayList的迭代器时,是将迭代器里的数据引用指向当前引用指向的数据对象,无论未来发生什么写操作,都不会再更改迭代器里的数据对象引用,所以迭代器也很安全。

例如下面的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);//复制到新数组中,长度+1
        newElements[len] = e;//在新数组中添加元素
        setArray(newElements);//将新数组设置给array
        return true;
    } finally {
        lock.unlock();
    }
}

0:先加锁

1:  先获取当前集合数组,数组是用volatile修饰的。

2:通过Arrays.copyOf得到一个新的数组。

3:在新的数组上增加元素。newElements[len]=e

4:  把array引用指向新的数组。setArray(newElements)

指定位置添加元素:

 

public void add(int index, E element) {
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
        // 获取旧数组
        Object[] elements = getArray();
        int len = elements.length;
        // 检查是否越界, 可以等于len
        if (index > len || index < 0)
            throw new IndexOutOfBoundsException("Index: "+index+
                                                ", Size: "+len);
        Object[] newElements;
        int numMoved = len - index;
        if (numMoved == 0)
            // 如果插入的位置是最后一位
            // 那么拷贝一个n+1的数组, 其前n个元素与旧数组一致
            newElements = Arrays.copyOf(elements, len + 1);
        else {
            // 如果插入的位置不是最后一位
            // 那么新建一个n+1的数组
            newElements = new Object[len + 1];
            // 拷贝旧数组前index的元素到新数组中
            System.arraycopy(elements, 0, newElements, 0, index);
            // 将index及其之后的元素往后挪一位拷贝到新数组中
            // 这样正好index位置是空出来的
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        // 将元素放置在index处
        newElements[index] = element;
        setArray(newElements);
    } finally {
        // 释放锁
        lock.unlock();
    }
}
//添加一个不存在于集合中的元素。
public boolean addIfAbsent(E e) {
    // 获取元素数组
    Object[] snapshot = getArray();
    //已存在返回false,否则添加
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
        addIfAbsent(e, snapshot);
}

private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 重新获取旧数组
        Object[] current = getArray();
        int len = current.length;
        // 如果快照与刚获取的数组不一致,说明有修改,减少indexOf需要遍历的元素个数
        if (snapshot != current) {
            // 重新检查添加元素e是否在current里,减少indexOf的对比次数
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                //判断是否有其他线程修改了数组,snapshot中肯定不会存在e、前面已经进行过indexOf判断,
                //所以current[i] != snapshot[i]如果返回false(不考虑并发下是相等的),则eq判断不用进行
                //for循环完毕都没有返回false的话,indexOf判断只需要判断current和snapshot的差集
                if (current[i] != snapshot[i] && eq(e, current[i]))
                    return false;
            if (indexOf(e, current, common, len) >= 0)
                    return false;
        }
        // 拷贝一份n+1的数组
        Object[] newElements = Arrays.copyOf(current, len + 1);
        // 将元素放在最后一位
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        // 释放锁
        lock.unlock();
    }
}

获取指定位置的元素:

public E get(int index) {
    return get(getArray(), index);
}
final Object[] getArray() {
    return array;
}
//私有方法
private E get(Object[] a, int index) {
    return (E) a[index];
}

这个方法是线程不安全的,因为这个分成了两步,分别是获取数组和获取元素,而且中间过程没有加锁。假设当前线程在获取数组(执行getArray())后,其他线程修改了这个CopyOnWriteArrayList,那么它里面的元素就会改变,但此时当前线程返回的仍然是旧的数组,所以返回的元素就不是最新的了,这就是写时复制策略产生的弱一致性问题

使用场景及优点

并发容器用于读多写少的并发场景。比如白名单,黑名单等场景。

读操作可能会远远多于写操作的场景。比如,有些系统级别的信息,往往只需要加载或者修改很少的次数,但是会被系统内所有模块频繁的访问。对于这种场景,我们最希望看到的就是读操作可以尽可能的快,而写即使慢一些也没关系。

CopyOnWriteArrayList 的思想比读写锁的思想更进一步。为了将读取的性能发挥到极致,CopyOnWriteArrayList 读取是完全不用加锁的,更厉害的是,写入也不会阻塞读取操作,也就是说你可以在写入的同时进行读取,只有写入和写入之间需要进行同步,也就是不允许多个写入同时发生,但是在写入发生时允许读取同时发生。这样一来,读操作的性能就会大幅度提升。

读写分离

缺点

内存占用,弱一致性

 CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差,但是读操作因为操作的对象和写操作不是同一个对象,读之间也不需要加锁,读和写之间的同步处理只是在写完后通过一个简单的“=”将引用指向新的数组对象上来,这个几乎不需要时间,这样读操作就很快很安全,适合在多线程里使用,绝对不会发生ConcurrentModificationException,所以最后得出结论:CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存,黑白名单。

 

posted @ 2021-03-08 13:18  蒙恬括  阅读(205)  评论(0编辑  收藏  举报