13.CopyOnWriteArrayList源码解析

1.简介
在ArrayList的类注释上,JDK就有相应提示,如果要把ArrayList作为共享变量的话,是线程不安全的,推荐开发者自己加锁或者使用Collections的synchronizedList方法,其实JDK还提供了另外一种线程安全的List,叫做CopyOnWriteArrayList,这个List具有以下特征。

  1. 是线程安全的,多线程环境下可以直接使用,无需加锁。
  2. 通过锁 + 数组拷贝 + volatile关键字保证了线程安全。
  3. 每次数组操作,都会把数组拷贝一份,在新数组上进行操作,操作成功之后再赋值回去。

2.架构
从架构上来说,CopyOnWriteArrayList数据结构和ArrayList是一致的,底层是数组,只不过CopyOnWriteArrayList在对数组进行操作的时候,需要按以下思路进行。加锁、从原数组中拷贝出新数组、在新数组上进行操作,并把新数组赋值给数组容器和解锁。除了加锁之外,CopyOnWriteArrayList的底层数组还被volatile关键字修饰,意思是一旦数组被修改,其它线程立马能够感知到。整体上来说,CopyOnWriteArrayList就是利用锁 + 数组拷贝 + volatile关键字保证了List的线程安全。

3.类注释

  1. 所有的操作都是线程安全的,因为操作都是在新拷贝数组上进行的。
  2. 数组的拷贝虽然有一定的成本,但往往比一般的替代方案效率高。
  3. 迭代过程中,不会影响到原来的数组,也不会抛出ConcurrentModificationException异常。

4.新增
新增有很多种情况,如新增到数组尾部、新增到数组某一个索引位置、批量新增等等,操作的思路就是架构中所说的思路,下面以新增到数组尾部的方法为例来分析其实现方式,具体源码如下所示。
源码

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
	public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        //加锁
        lock.lock();
        try {
        	//获取原数组
            Object[] elements = getArray();
            int len = elements.length;
            //将原数组拷贝到新数组里,新数组长度是原数组长度加1,因为新增会多一个元素
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //在新数组里新增元素,新元素直接放在数组的尾部
            newElements[len] = e;
            //替换原来的数组
            setArray(newElements);
            return true;
        } finally {
        	//finally里面释放锁,保证即使try发生了异常,仍然能够释放锁  
            lock.unlock();
        }
    }
}

源码解析

  1. 从源码中发现整个add过程都是在持有锁的状态下进行的,通过加锁,来保证同一时刻只能有一个线程能够对数组进行操作。
  2. 除了加锁之外,还会从老数组中创建出一个新数组,然后把老数组的值拷贝到新数组中,这时候就有一个问题,都已经加锁了,为什么需要拷贝数组,而不是在原来数组上面进行操作呢?主要原因有两个,一是volatile关键字修饰的是数组,如果简单的在原来数组上修改其中某几个元素的值,是无法触发可见性的,因此必须通过修改数组的内存地址才行,也就是说需要对数组进行重新赋值才行。二是在新的数组上进行拷贝,对老数组没有任何影响,只有新数组完全拷贝完成之后,外部才能访问到,降低了在赋值过程中,对 老数组数据变动的影响。
  3. CopyOnWriteArrayList通过加锁 + 数组拷贝+ volatile来保证线程安全,每一个要素都有着其独特的含义。加锁操作保证了同一时刻数组只能被一个线程操作。数组拷贝保证了数组的内存地址被修改后触发volatile的可见性,其它线程可以立马感知数组已被修改。volatile关键字保证了值被修改后,其它线程能够立马感知最新值。三个要素缺一不可,如只使用1和3,去掉2,这样当修改数组中某个值时,并不会触发volatile的可见性,只有当数组内存地址被修改后,才会把最新值通知给其他线程。

5.删除
删除也有多种情况,如删除某个索引位置的数据、批量删除等等,下面以删除某个索引位置数据的方法举例来分析其实现方式,具体源码如下所示。
源码

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
	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 {
            	//如果删除的数据在数组的中间,则分三步走
            	//1.新数组长度是原数组长度减1,因为是减少一个元素
            	//2.从头开始拷贝数据到数组新位置
            	//3.从新位置拷贝数据到数组尾部
                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();
        }
    }
}

源码解析

  1. 从源码中可以知道,删除一个元素主要分三步走,加锁、判断删除索引的位置,从而进行不同策略的拷贝、解锁。
  2. 代码整体的结构风格也比较统一,锁 + try finally + 数组拷贝,锁被final修饰的,保证了在加锁过程中,锁的内存地址肯定不会被修改,finally保证锁一定能够被释放,数组拷贝是为了删除其中某个位置的元素。

6.查找
indexOf方法的主要用处是查找元素在数组中的下标位置,如果元素存在就返回元素的下标位置,元素不存在的话返回-1,不但支持null值的搜索,还支持正向和反向的查找,下面以正向查找为例,通过源码来说明一下其底层的实现方式,具体源码如下所示。
源码

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
	/**
     * 查找元素在数组中的下标位置
     * 
     * @param o  需要搜索的元素
     * @param elements 搜索的目标数组
     * @param index  搜索开始的位置
     * @param fence  搜索结束的位置
     * @return int
     */
	private static int indexOf(Object o, Object[] elements, int index, int fence) {
		//支持对null的搜索
        if (o == null) {
            for (int i = index; i < fence; i++)
            	//找到第一个null值,返回下标索引的位置
                if (elements[i] == null)
                    return i;
        } else {
        	//通过equals方法来判断元素是否相等,如果相等,返回元素的下标位置
            for (int i = index; i < fence; i++)
                if (o.equals(elements[i]))
                    return i;
        }
        return -1;
    }
}

源码解析
indexOf方法在CopyOnWriteArrayList内部使用也比较广泛,如在判断元素是否存在时、删除元素方法中校验元素是否存在时,都会使用到indexOf方法。indexOf方法通过一次for循环来查找元素,在调用此方法时,需要注意如果找不到元素时,返回的是-1。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值