0
点赞
收藏
分享

微信扫一扫

juc并发包之CopyOnWriteArrayList

生态人 2022-04-14 阅读 64
java

一. 线程不安全的ArrayList

1、为什么说ArrayList是线程不安全的:

add()操作抛出数组越界异常;
add()操作会丢失元素;
set()操作去修改元素,get()操作去获取元素时,可以读到新值也可能读到旧值,无法保证一致性。
源码分析:

public boolean add(E e) {
    //确定添加元素之后,集合的大小是否足够,若不够则会进行扩容
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //插入元素
    elementData[size++] = e;
    return true;
}

场景1:多个线程都没进行扩容,但是执行了elementData[size++] = e;时,便会出现“数组越界异常”;
场景2:因为size++本身就是非原子性的,多个线程之间访问冲突,这时候两个线程可能对同一个位置赋值,就会出现“size小于期望值的结果”;
场景3:迭代器遍历修改报错
ArrayList是线程不安全的集合类,在单线程环境下,使用iterator进行遍历修改的时候会出现 java.util.ConcurrentModificationException并发修改异常,如下

import java.util.*;
public class Test {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        list.add("1w");
        list.add("2w");
        list.add("3w");
        Iterator<String> iterator = list.iterator();
        while (iterator.hasNext()){
            String cur = iterator.next();
            if(cur=="1w"){
                list.remove(cur);
            }
            System.out.println(cur);
        }
    }
}

该程序会报出ConcurrentModificationException异常:
在这里插入图片描述
这就是fail-fast机制在单线程环境下的体现。但是原理是是什么呢?

这里涉及到我们在ArrayList的源码中有一个变量modCount,该变量表明当前ArrayList结构修改的次数。而ArrayList中重新定义了一个Itr类实现了Iterator接口,在该接口中有一个变量expectedModCount,迭代以前expectedModCount等于modCount。
在这里插入图片描述
是我们在迭代的过程中在list中增加或者删除数据导致了list结构有所变化,modCount也变化了,不再等于expectedModCount。导致利用迭代器的next方法的时候,判断两者不相等,从而出现并发修改异常:
在这里插入图片描述
调用迭代器的next方法,会调用checkForComodification()方法进行一个检验:
在这里插入图片描述
既然了解了fail-fast的原理了,那么下面的例子出现问题我们应该也能知道是怎么回事了:就是因为在获取迭代器之后,对list的结构做了修改,导致expectedModCount与modCount不相等,之后调用next()从而检验两者是否相等,从而报出异常。

二 、CopyOnWriteArrayList

一:适用场景

1.读操作尽可能地快,而写即使慢一点也没有太大的关系

2.读多写少:热点数据;监听器:迭代操作远多余修改操作

3.CopyOnWriteArrayList可以在迭代的过程中修改内容,但是ArrayList不行。

4.CopyOnWrite的含义:创建新副本,读写分离。

二:CopyOnWriteArrayList缺点

1.数据一致性问题:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的数据马上能读到,请不要使用CopyOnWrite容器

2.内存占用问题:因为CopyOnWrite的写是复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存。

三:读写规则

1.读写锁:读读共享,其他都互斥(写写互斥,读写互斥,写读互斥)

2.读写锁规则的升级:读是完全不用加锁的,并且更厉害的是,写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待

源码分析:

CopyOnWriteArrayList构造函数:

public CopyOnWriteArrayList()

//Collection做初始化参数
public CopyOnWriteArrayList(Collection<? extends E> c)

//Array做初始化参数
public CopyOnWriteArrayList(E[] toCopyIn)

读操作

privateE get(Object[] a, intindex) {
    return(E) a[index];
}
 
publicE get(intindex) {
    returnget(getArray(), index);
}

直接读取,不需要加锁,因为即使读取过程中有其他线程改动List,也是开辟新的数组并在新数组上改动,旧数组对象始终是可用的。

2.写操作
在CopyOnWriteArrayList中写操作过程大致是这样的。在原有数组的基础上拷贝一份新的数组(容器副本),将改动操作在新数组上完成(即把新增元素加入新数组中),然后再把新数组对象的引用赋给CopyOnWriteArrayList的array。显然,在多线程环境中,为了保证线程安全,整个过程需要加锁。所以CopyOnWriteArrayList的写操作性能损耗是很大的,一方面需要竞争获取锁,另一方面需要进行复制操作。

下面以add(int index, E element)方法为例说明CopyOnWriteArrayList的修改操作:

//指定位置增加元素
    public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        //修改array前获取锁
        lock.lock();
        try {
        //获取原有array
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;
            if (numMoved == 0)
            //index=length,即数组尾部新增一个元素,同add(E element)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
            //两次复制
                newElements = new Object[len + 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            //新增元素
            newElements[index] = element;
            //将新数组引用赋值给array
            setArray(newElements);
        } finally {
        //释放锁
            lock.unlock();
        }
    }

除了add方法,还有remove、removeRange、addIfAbsent等其他修改操作原理都是一样的,都是新new一个数组对象,在新array上进行修改操作,完事后再将新数组引用赋值给实例变量array,当然修改操作都是需要加锁的。
修改:

 /**
     * Replaces the element at the specified position in this list with the
     * specified element.
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            if (oldValue != element) {
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

删除

 /**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).  Returns the element that was removed from the list.
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    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();
        }
    }

线程安全性

我们可以看到,CopyOnWriteArrayList内部的array数组对象从被创建,到这个对象生命结束,是不可变的。变的是array变量的引用值,每做一次修改操作,array变量就指向新生成的数组对象,原对象被gc,如此反复。这种方式核心思想是减少锁竞争,从而提高高并发时的读取性能,但一定程度上牺牲了写的性能。
由此可得:“写入时复制(Copy-On-Write)”容器的线程安全性在于:只有正确地发布一个事实不可变的对象,那么在访问该对象时就不需要做同步操作。这也就解释了为什么通过迭代器Iterator是不允许进行修改操作的了。

举报

相关推荐

0 条评论