从源码深度剖析 CopyOnWriteArrayList 线程安全集合,一起来看看吧

目录

1. 前言

2.CpoyOnWriteArrayList 原理简单概述

3. CopyOnWriteArrayList 源码分析

3.1 属性构造器解读

3.2 get 方法分析

3.3 add 方法分析

3.4 set 方法分析

3.5 remove 方法分析

4. 总结概括


1. 前言

使用过 ArrayList 集合的同学应该大致都知道,ArrayList 是一个非线程安全的集合;同样,Java也为我们提供了线程安全的 List 集合,只是用的频率没有 ArrayList 那么频繁,它就是我们本篇文章要说的 CopyOnWriteArrayList。

2.CpoyOnWriteArrayList 原理简单概述

CopyOnWriteArrayList 的原理不难理解它底层采用了加锁的方式保证线程安全并且加的是 Lock 锁而不是 Sychonized 锁

假如现在有两个线程,一个读线程A,一个写线程B,同时想要想数组中添加元素,读线A程就会读取当前内存中 CopyOnWriteArrayList 集合,写线程B则是会将内存中的 CopyOnWriteArrayList 集合对象复制一份新的,在新复制的集合中执行添加操作,添加操作完成之后再将新的集合赋值给原来老的集合,并且这个过程中写线程B会获取唯一的 Lock 锁,其它写线程会阻塞等待,实现读写分离。那么假如说有第三个写线程C也想要执行写数据操作,就需要等待写线程B操作完成之后释放 Lock 锁自己获取到 Lock 锁之后才能去执行写入操作。

3. CopyOnWriteArrayList 源码分析

3.1 属性构造器解读

下面是我粘贴的一部分属性,get,set方法,构造方法。

(1)可以看到在 CopyOnWriteArrayList 内部它定义了一个 Lock 锁对象;

(2)底层定义了一个名为 array 的对象数组;

(3)无参构造可以看出调用无参构造会将 array 数组对象的长度设置为 0,只有在进行存储元素的时候才回去扩容;

public class CopyOnWriteArrayList
    implements List, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

    /**
     * Gets the array.  Non-private so as to also be accessible
     * from CopyOnWriteArraySet class.
     */
    final Object[] getArray() {
        return array;
    }

    /**
     * Sets the array.
     */
    final void setArray(Object[] a) {
        array = a;
    }

    /**
     * Creates an empty list.
     */
    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }
3.2 get 方法分析

下面是 CopyOnWriteArrayList 的 get 获取元素的方法,这里它 index 和数组的长度大小都没有做判断,所以很有可能会出现索引越界异常;

get 获取元素的方法很简单,没有出现加锁的行为

public E get(int index) {
// 直接返回对象 index 位置的元素
        return get(getArray(), index);
    }
3.3 add 方法分析
public void add(int index, E element) {
// 获取 Lock 锁
        final ReentrantLock lock = this.lock;
// 调用方法上锁
        lock.lock();
        try {
// 获取内存的数组对象并赋值为 elements
            Object[] elements = getArray();
// 定义一个变量 len 获取数组的长度
            int len = elements.length;
// 判断方法的参数 index 是否越界或合法
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
// 定义一个新数组对象 newElements
            Object[] newElements;
// 定义一个变量 numMoved 接收数组长度 - index 的值
            int numMoved = len - index;
// 如果 numMoved 为0,则说明要把新添加的元素放在数组的最后
            if (numMoved == 0)
// 调用调用 copyOf 方法将原来数组中的数据全部复制到 newElements中,
// 并在数组末尾添加上新的元素
                newElements = Arrays.copyOf(elements, len + 1);
            else {
// 如果 numMoved 不为0,则说明要将该元素添加在数组中间的某个位置
// 先将新数组的长度 + 1
                newElements = new Object[len + 1];
// 将老数组 0~index 之间的数据全部复制到新数组中
                System.arraycopy(elements, 0, newElements, 0, index);
// 再将 index~数组最后的数据全部复制到新数组中
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
// 将要添加的元素 element 添加到新数组的 index 的位置
            newElements[index] = element;
// 将老数组地址值赋值给新数组对象
            setArray(newElements);
        } finally {
// 操作完毕,最后释放锁
            lock.unlock();
        }
    }
3.4 set 方法分析
public E set(int index, E element) {
// 获取 Lock 锁
        final ReentrantLock lock = this.lock;
// 调用方法上锁
        lock.lock();
        try {
// 获取内存的数组对象并赋值给一个新的数组对象 elements
            Object[] elements = getArray();
// 获取 index 处的元素
            E oldValue = get(elements, index);
// 判断 oldValue 和要插入的元素是否相等
            if (oldValue != element) {
// 获取数组的长度
                int len = elements.length;
// 将原本的数组数据复制到新数组 newElements 中
                Object[] newElements = Arrays.copyOf(elements, len);
// 将 element 放置到新数组的 index 处
                newElements[index] = element;
// 将新数组覆盖原来的数组
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
// 进入 else ,说明要set的元素在数组中已经存在,直接返回原数组
                setArray(elements);
            }
// 返回位置 index 处的老的元素
            return oldValue;
        } finally {
// 操作完成,释放 lock 锁
            lock.unlock();
        }
    }
3.5 remove 方法分析
public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
// 获取数组长度
            int len = elements.length;
// 获取 index 处的元素
            E oldValue = get(elements, index);
// 定义 numMoved 计算出要移动的元素的数量
            int numMoved = len - index - 1;
// 如果 numMoved 为0,说明要删除的元素恰好是数组的最后一个元素
            if (numMoved == 0)
// 覆盖原来的数组
                setArray(Arrays.copyOf(elements, len - 1));
            else 
// numMoved 不为0,则定义一个新数组,长度为原来的数组长度-1
                Object[] newElements = new Object[len - 1];
// 将 0~index 处的元素复制到新数组中去
                System.arraycopy(elements, 0, newElements, 0, index);
// 将 index+1~数组最后的元素移动到新数组中
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
// 覆盖原来的数组
                setArray(newElements);
            }
// 返回删除的元素值
            return oldValue;
        } finally {
// 操作完毕,释放 lock 锁
            lock.unlock();
        }
    }

4. 总结概括

经过上面对 add 添加方法,get 获取方法,set 修改方法,remove 删除方法的分析,其实同学们也可以看出,无非就是在原来 ArrayList 集合的基础上添加了一把 lock 。

在做增,改,删三种操作的时候,搭配上 copy 复制数组的思想,就可以做到线程安全,这就是 CopyOnWriteArrayList 线程安全的核心设计思想,不算特别难理解。

你可能感兴趣的:(java,开发语言)