Java容器源码(三)——CopyOnWriteArrayList源码分析(基于JDK8)

文章目录

    • (一)、概述
    • (二)、适用场景
    • (二)、类名
    • (三)、成员变量
    • (四)、构造方法
    • (五)、set方法
    • (六)、add方法
    • (七)、remove方法
    • (八)、addIfAbsent方法

更多Java容器源码分析可以参考:Java容器源码分析系列(持续更新中!)

(一)、概述

  1. CopyOnWriteArrayList的特点主要是实现了读写分离,读操作时不加锁,写操作才进行加锁,防止并发时写入导致数据丢失
  2. 在进行写操作的时候,不是在原数组上直接修改,而是创建一个新的数组,对原数组进行复制修改等操作后,再将指针指向新的数组。

(二)、适用场景

  1. CopyOnWriteArrayList主要使用于读多写少的应用场景,这会大大提高读的性能
  2. 缺点:
  • List item会占用较多内存,因为在写操作的时候,都需要创建一个新的数
  • 可能会出现数据不一致的情况,读操作可能读到过时的数据,因为写操作不在原数组上进行,可能还未来得及指向新数组。

(二)、类名

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
  1. CopyOnWriteArrayList实现了List接口、RandomAccess接口、Clonable接口以及Serializable接口
  2. List接口:主要提供一些增删改查的方法
  3. RandomAccess接口:这是一个空的接口,实现了这个接口,就可以实现随机访问
  4. Cloneable接口:实现了这个接口,能够实现克隆功能。
  5. Serializable接口:可以实现序列化功能。

(三)、成员变量

    /**
    * 重入锁ReentrantLock,同时也是线程独占锁,用来保证线程安全
    */
    final transient ReentrantLock lock = new ReentrantLock();

    /**
    * 用来存放数组对象
    */
    private transient volatile Object[] array;
  1. lock:用来在更新修改操作时保证线程独占
  2. array:用来存放真实的数据对象

(四)、构造方法

 	/**
 	* 获得数组对象
 	*/
    final Object[] getArray() {
        return array;
    }

    /**
     * 设置数组对象
     */
    final void setArray(Object[] a) {
        array = a;
    }

    /**
     * 无参构造方法
     */
    public CopyOnWriteArrayList() {
        //创建一个长度为0的空数组
        setArray(new Object[0]);
    }

    /**
	* 参数为集合对象的构造方法
     */
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        //查看参数c是否本身已经是CopyOnWriteArrayList
        if (c.getClass() == CopyOnWriteArrayList.class)
            //如果本身就是,那就直接获取CopyOnWriteArrayList中的数组对象
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        else {
        	//否则,先将参数c转换为数组对象
            elements = c.toArray();
            // 查看element中的元素是否是Object类型的
            if (elements.getClass() != Object[].class)
            	//如果不是,就重新创建一个Object类型的数组,将元素复制进去
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        //通过setArray方法设置数组对象
        setArray(elements);
    }

    /**
     * 参数为数组对象的有参构造方法
     */
    public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

  1. 首先,构造方法中基本都设计了两个比较简单的方法:setArray()和getArray()。两个方法的实现都较为简单,源码也已给出。
  2. CopyOnWriteArrayList():无参构造方法。方法中创建了一个长度为0的空数组
  3. CopyOnWriteArrayList(Collection c):参数为集合对象的构造方法,该方法可以将c对象中的数据复制到this对象中
  4. CopyOnWriteArrayList(E[] toCopyIn):参数为数组的有参构造方法,可以将数组的元素复制到this对象中。

(五)、set方法

	/**
	* set方法,可以设置数组对象中的值
	*/
    public E set(int index, E element) {
    	//获得锁对象
        final ReentrantLock lock = this.lock;
        //锁住
        lock.lock();
        try {
        	//获取数组对象
            Object[] elements = getArray();
            //获取elements对象在index位置上的元素
            E oldValue = get(elements, index);
			//如果旧值不等于新值
            if (oldValue != element) {
            	//获取数组对象的长度
                int len = elements.length;
                //复制原来的数组对象
                Object[] newElements = Arrays.copyOf(elements, len);
                //将新数组对象中index位置上的值设置为新值
                newElements[index] = element;
                //将新数组对象写会CopyOnWriteArrayList中
                setArray(newElements);
            } else {
                // 否则无需修改原来对的数组对象
                setArray(elements);
            }
            //返回更新前的值
            return oldValue;
        } finally {
            //解锁
            lock.unlock();
        }
    }
  1. 首先获得锁对象,将方法锁定
  2. 第二步,查看要更新的值和原来的旧值是否一样
  3. 如果一样就选择不更新
  4. 如果不一样的话,先将原来的数组拷贝一份出来,成为新数组
  5. 然后将新数组上index位置上的元素设置为新值element
  6. 然后将新数组写回
  7. 最后,记得解除锁定

(六)、add方法

	 /**
     *  在末尾添加上元素的add方法
     */
    public boolean add(E e) {
    	//首先,获得锁对象
        final ReentrantLock lock = this.lock;
        //锁住
        lock.lock();
        try {
        	//获得原数组对象
            Object[] elements = getArray();
            //获得原数组对象的长度
            int len = elements.length;
            //创建一个长度为len+1的新数组对象,并将原数组中的元素复制进去
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //在末尾添加上元素e
            newElements[len] = e;
            //将新数组重新设置到this对象中
            setArray(newElements);
            //返回true
            return true;
        } finally {
        	//解锁
            lock.unlock();
        }
    }

    /**
     * 在指定位置上添加元素的add方法
     */
    public void add(int index, E element) {
   	    //首先,获得锁对象
        final ReentrantLock lock = this.lock;
        //锁住
        lock.lock();
        try {
        	//获得原数组对象
            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;
            //如果是在末尾位置插入的话,就创建一个长度为len+1的新数组,将原数组中的数据复制进去
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
            	//否则,同样创建一个长度为len+1的数组
                newElements = new Object[len + 1];
                //从原数组的0到index-1位置上的元素复制到新数组中
                System.arraycopy(elements, 0, newElements, 0, index);
                //将原数组的index位置到末尾上的元素复制到新数组中
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            //在index位置上设置元素为element
            newElements[index] = element;
            //设置回this对象中
            setArray(newElements);
        } finally {
        	//解锁
            lock.unlock();
        }
    }
  1. 两个add方法的实现都差不多,这里选择第二个方法进行讲解
  2. add(int index, E element)方法:是在指定位置上添加元素的
  3. 首先获得锁对象,并且锁住这个方法
  4. 获得原数组的对象,计算长度,查看想插入的位置index是否合法
  5. 然后计算插入该元素需要移动元素的个数
  6. 如果numMoved=0,那说明想要在数组的末尾插入元素,那么就直接将原数组的元素复制到新数组中
  7. 如果numMoved>0,就将index位置空出来,其他元素全部移动到新数组中
  8. 然后在新数组中添加上index位置上的值
  9. 最后,要记得解锁。

(七)、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)
            	//直接将0~len-1上的元素拷贝到新数组中,并将指向新数组
                setArray(Arrays.copyOf(elements, len - 1));
            else {
            	//创建一个长度为len-1的数组对象
                Object[] newElements = new Object[len - 1];
                //将原数组中0~index-1的元素复制到新数组中
                System.arraycopy(elements, 0, newElements, 0, index);
                //将原数组中index+1到末尾的元素复制到新数组中
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                 //指向新的数组对象
                setArray(newElements);
            }
            //返回旧值
            return oldValue;
        } finally {
        	//解锁
            lock.unlock();
        }
    }

    /**
     * 根据对象进行删除
     */
    public boolean remove(Object o) {
    	//获取原数组对象
        Object[] snapshot = getArray();
        //计算o对象在原数组中的位置
        int index = indexOf(o, snapshot, 0, snapshot.length);
        //三元表达式,如果index小于0表示对象不存在,大于0则删除对象
        return (index < 0) ? false : remove(o, snapshot, index);
    }

    /**
	*	
     */
    private boolean remove(Object o, Object[] snapshot, int index) {
    	//获得锁对象
        final ReentrantLock lock = this.lock;
        //锁住
        lock.lock();
        try {
        	//获得原数组对象
            Object[] current = getArray();
            //获得原数组的长度
            int len = current.length;
            //如果当前的数组对象和传入的snapshot数组对象不一样,说明被其他线程修改了
            if (snapshot != current) findIndex: {
            	//获取两者的最小长度
                int prefix = Math.min(index, len);
                //遍历两者共有的长度
                for (int i = 0; i < prefix; i++) {
                    //如果两个数组在i位置上的元素不一样,且o对象等于当前数组上i位置上的元素
                    if (current[i] != snapshot[i] && eq(o, current[i])) {
                    	//要删除的位置就是i位置
                        index = i;
                        //结束循环
                        break findIndex;
                    }
                }
                //查看index位置是否越界
                if (index >= len)
                    return false;
                if (current[index] == o)
                    break findIndex;
                //前面查找失败,就继续从index往后查找
                index = indexOf(o, current, index, len);
                if (index < 0)
                    return false;
            }
            //创建一个长度为len-1的新数组对象
            Object[] newElements = new Object[len - 1];
            //将原数组中除了index位置上的元素,全部搬迁到新数组中
            System.arraycopy(current, 0, newElements, 0, index);
            System.arraycopy(current, index + 1,
                             newElements, index,
                             len - index - 1);
            //指向新数组
            setArray(newElements);
            //返回true
            return true;
        } finally {
        	//解锁
            lock.unlock();
        }
    }
  1. remove方法的基本实现其实也差不多,这里就选择第二个和第三个方法进行讲解。
  2. 第二个方法传入的是一个Object对象,他会在这个方法里面查看原数组中是否含有这个o对象,如果含有就调用第三个remove方法。因为这里只是读取,所以不用加锁
  3. 第三个remove方法中,参数包含了第二个方法传来的数组snapshot。
  4. 首先获得锁对象,锁住方法。
  5. 然后判断snapshot对象是否和当前数组对象current相同,以此来判断是否在这间隙被修改过
  6. 然后注意遍历current数组中的元素,看是否含有o对象
  7. 接着遍历完后,创建一个长度为len-1的新数组对象,将原数组中的元素搬到新数组中。
  8. 最后指向新数组,释放锁。

(八)、addIfAbsent方法

	 /**
	 * 还有一个参数e的addIfAbsent方法
     */
    public boolean addIfAbsent(E e) {
    	//获得原数组对象
        Object[] snapshot = getArray();
        //三元表达式,如果原数组中含有这个元素了,就直接返回false
        //如果不含有,就调用方法进行添加
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
            addIfAbsent(e, snapshot);
    }

    /**
	 * 两个参数对的addIfAbsent方法
     */
    private boolean addIfAbsent(E e, Object[] snapshot) {
    	//获得锁对象
        final ReentrantLock lock = this.lock;
        //锁住
        lock.lock();
        try {
        	//获得原数组对象
            Object[] current = getArray();
            //获得原数组对象的长度
            int len = current.length;
            //如果当前原数组对象和传入的snapshot数组对象不一致,说明被修改了
            if (snapshot != current) {
                // 选择两者的最小值
                int common = Math.min(snapshot.length, len);
                //遍历两者的最小长度
                for (int i = 0; i < common; i++)
                	//如果两者元素不一样,且当前原数组中含有e对象就直接返回false
                    if (current[i] != snapshot[i] && eq(e, current[i]))
                        return false;
                //从common位置开始遍历完所有元素,如果current数组中存在就返回false
                if (indexOf(e, current, common, len) >= 0)
                        return false;
            }
            //创建一个长度为len+1的数组对象,并将原数组的元素复制到新数组中
            Object[] newElements = Arrays.copyOf(current, len + 1);
            //设置最末尾的元素为e
            newElements[len] = e;
            //指向新数组
            setArray(newElements);
            //返回true
            return true;
        } finally {
        	//解锁
            lock.unlock();
        }
    }
  1. addIfAbsent方法主要是用来判断数组中是否含有对象e,如果不含有,就添加到数组中
  2. 首先是addIfAbsent(E e)这个方法,使用indexOf函数查看是否含有e对象,如果不含有就调用方法进行添加。
  3. addIfAbsent(E e, Object[] snapshot)方法接收了第一个方法传来的原数组作为参数
  4. 首先获得锁对象,锁住方法
  5. 获得原数组对象,并获取原数组对象的长度
  6. 如果当前原数组对象和传入的snapshot数组对象不一致,说明被修改了,那就遍历current数组中是否含有e对象
  7. 接着遍历完成后,创建一个长度为len+1的数组对象,并将原数组的元素复制到新数组中,设置最末未的元素为e
  8. 最后指向新数组,释放锁

你可能感兴趣的:(Java基础)