ArrayList和CopyOnWriteArrayList

1,479 阅读8分钟

ArrayList

ArrayList是Java集合中最常用的一个数据结构,所以学习它的实现原理十分有必要。

ArrayList的属性

//ArrayList默认的容量
private static final int DEFAULT_CAPACITY = 10;
//用于判断当前是否为初始化扩容操作
private static final Object[] EMPTY_ELEMENTDATA = {};
//用于判断当前是否为初始化扩容操作,与上面的区别在于上面属性是使用空构造函数进行实例化,而这个则使用带参构造函数,参数为0的时候使用
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//用于存储真实数据
transient Object[] elementData; 
//元素的当前个数
private int size;
//设置ArrarList最大容量,其实ArrayList最大容量有可能为Integer.MAX_VALUE;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

ArrayList构造函数

//initialCapacity:初始化容量大小
public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            //如果初始化容量大小大于0,那么就初始化元素数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            //初始化容量为0,就将元素数组赋值为空元素,用于后面扩容判断操作
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            //初始化容量为负数,那么就会抛异常
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
}
//空参构造函数
public ArrayList() {
        //初始化容量为0,就将元素数组赋值为空元素,用于后面扩容判断操作
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//c:集合类
//该构造函数用于将集合类中的数据放置到新的ArrayList当中
public ArrayList(Collection<? extends E> c) {
        //给元素数组赋值
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            //有可能参数中的集合类中的数据不是Object类,那么就要设置成Object类
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            //如果参数中的集合类的元素数据为null,将其ArrayList中的元素数据设置为空
            this.elementData = EMPTY_ELEMENTDATA;
        }
}

在ArrayList类中最常用的其实就是增删改查这些操作,接下来窥探这几个方法的源码

add方法

public boolean add(E e) {
    //在添加元素之前,需要先对当前存储数据的元素数组的容量进行确认,如果容量不够那么就需要进行扩容,需要的话就进行扩容
    ensureCapacityInternal(size + 1);
    //将添加的元素赋值到数组中
    elementData[size++] = e;
    return true;
}
private void ensureCapacityInternal(int minCapacity) {
    //calculateCapacity():计算元素数组的容量
    //ensureExplicitCapacity():用于确保显示容量
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //用于判断当前元素数组是否为初始化的元素数组
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        //是的话返回DEFAULT_CAPACITY和minCapacity中的最大值
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //若不为初始化元素数组,则返回添加一个元素之后的容量大小
    return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
    //modCount:用于记录ArrayList的修改次数
    modCount++;
​
    //如果需要的最小容量大小大于当前存储元素数组的大小,那么就进行扩容的操作
    if (minCapacity - elementData.length > 0)
        //根据最小容量进行扩容操作
        grow(minCapacity);
}
private void grow(int minCapacity) {
    // overflow-conscious code
    //计算当前元素数组中的大小
    int oldCapacity = elementData.length;
    //计算新的容量,增加为原来容量的一半
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //该判断语句用于判断是否为初始化操作
    if (newCapacity - minCapacity < 0)
        //是的话就设置ArrayList的容量为默认容量10;
        newCapacity = minCapacity;
    //新的容量是否大于ArrayList所允许的最大容量
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //由于判断minCapacity是否也大于MAX_ARRAY_SIZE,如果大于的话就会将新的容量设置为Integer.MAX_VALUE,否则的话就会设置成MAX_ARRAY_SIZE
        newCapacity = hugeCapacity(minCapacity);
    //将当前已经存放到元素数组中的数据复制到新的扩容之后的数组当中
    elementData = Arrays.copyOf(elementData, newCapacity);
}
​
public void add(int index, E element) {
    //进行范围检查,如果要插入的index大于了当前元素数组的数据个数就报错
    rangeCheckForAdd(index);
    //跟上面代码一样,用于确保内容容量
    ensureCapacityInternal(size + 1); 
    //index位置后面的size-index元素移动到index+1位置上,也就是将index位置和index之后的元素向后移动一个位置
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    //给元素数组中index位置赋值
    elementData[index] = element;
    //元素大小+1
    size++;
}

remove方法

//移除索引
public E remove(int index) {
    //检测要移除的索引位置能否移除,如果要移除的索引位置大于元素个数的话就会报错
    rangeCheck(index);
    //modCount+1
    modCount++;
    //获取到要移除索引位置的元素数据
    E oldValue = elementData(index);
    //计算出要移除索引位置之后的元素个数
    int numMoved = size - index - 1;
    //如果要移除的索引位置后面还有元素的话就将后面的元素整体向前移动一个位置
    if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,
    numMoved);
    //移动之后将元素数组的最后一位置为null,方便GC回收
    elementData[--size] = null; // clear to let GC do its work
    //返回移除索引对应的的元素
    return oldValue;
}
//移除ArrayList中的具体元素数据
public boolean remove(Object o) {
    //判断要移除的元素是否为null
    if (o == null) {
        //通过循环的方式来查找要移除的元素数据在数组中的哪个索引位置
        for (int index = 0; index < size; index++)
            //找到元素为null的索引位置
            if (elementData[index] == null) {
                //跟上面移除元素一样
                fastRemove(index);
                return true;
             }
    } 
    else {
    for (int index = 0; index < size; index++)
        //判断当前数组元素是否为要移除的元素
        if (o.equals(elementData[index])) {
            fastRemove(index);
            return true;
        }
    }
    //如果没有找到,则返回false
    return false;
}

set方法

public E set(int index, E element) {
    //检查要修改的索引位置是否大于当前元素个数,大于则报错
    rangeCheck(index);
    //查找出要修改索引位置的旧数据
    E oldValue = elementData(index);
    //进行修改
    elementData[index] = element;
    //返回旧数据
    return oldValue;
}

get方法

public E get(int index) {
    //检查index位置
    rangeCheck(index);
    //返回索引对应的元素
    return elementData(index);
}

ArrayList总结:

1、ArrayList是一个以数组作为底层的数据结构,在进行数据的修改和查询的时候会非常的迅速。

2、在创建ArrayList的时候,如果使用的是参数为初始化容量的构造器创建对象的话,默认ArrayList的容量为10

3、ArrayList扩容会在原有的元素数组的大小上增加元素数组的一半。

4、ArrayList的最大容量在大多数情况下都为Integer.MAX_VALUE-8,但在特殊的情况下会为Integer.MAX_VALUE

ArrayList在单线程环境下使用起来非常的快捷方便,但是在多线程环境下是否能够保证线程安全呢?答案是否定

通过阅读add方法的源码可以发现,ArrayList是否进行扩容是根据size的小大来判断。首先会根据size+1的值进行判断是否需要扩容,如果size的大小大于存储元素数组的长度,那么就需要进行扩容操作。然后才会将size置为size+1之后的值。接下来通过一个例子来讲解为什么ArrayList会出现线程安全问题。

假如现在有两个线程需要向ArrayList添加元素,当前在两个线程来看size都为0,当两个线程开始向ArrayList添加元素,线程A先将元素放到位置0,此时CPU调度线程A暂停,B线程开始执行,size还是为0,所以B线程也将元素放到位置0。然后线程A和线程B都继续执行,size增加,这个时候元素实际上只有一个,但是size为2,这就是线程安全问题。 接下来通过代码来验证

public class Test {
    public static void main(String[] args) {
        ArrayList<String> list=new ArrayList<>();
       for(int i=0;i<10;i++){
           new Thread(()->{
               list.add(UUID.randomUUID().toString());
               System.out.println(list);
           }).start();
       }
    }
}
/*
Exception in thread "Thread-9" Exception in thread "Thread-6" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
    at java.util.ArrayList$Itr.next(ArrayList.java:859)
    at java.util.AbstractCollection.toString(AbstractCollection.java:461)
    at java.lang.String.valueOf(String.java:2994)
    at java.io.PrintStream.println(PrintStream.java:821)
    at com.itheima.test.Test5.lambda$main$0(Test.java:17)
    at java.lang.Thread.run(Thread.java:748)
*/

那如何保证ArrayList的线程安全问题呢?有三个方式

1、锁机制

2、CopyOnWriteArrayList类

3、Collections.synchronizedList()

接下来对CopyOnWriteArrayList类从源码的角度来查看分别是如何实现线程安全的。

CopyOnWriteArrayList类

属性

//创建锁对象
final transient ReentrantLock lock = new ReentrantLock();
//用于存储元素的数组,只能通过setArray和getArray方法来修改数组和获取数组
private transient volatile Object[] array;

方法

//添加元素
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);
        //赋值
        newElements[len] = e;
        //将赋值后的新数组赋值给CopyOnWriteArrayList类中负责存储元素的array属性
        setArray(newElements);
        return true;
    } finally {
        //释放锁
        lock.unlock();
    }
}
//使用final修饰方法,防止继承CopyOnWriteArrayList重写getArray方法,从而对array进行修改,导致破坏了CopyOnWriteArrayList类的线程安全性。
final Object[] getArray() {
    return array;
}
//与上面类似
final void setArray(Object[] a) {
    array = a;
}
//删除元素
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;
        //如果元素数量为0,就说明要移除的元素为最后一个
        if (numMoved == 0)
            //将索引位置之前的所有元素拷贝赋值给array
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            //创建新的元素数组
            Object[] newElements = new Object[len - 1];
            //将要移除索引之前的全部元素赋值从elements中赋值到新的元素数组中
            System.arraycopy(elements, 0, newElements, 0, index);
            //将要移除索引之后的全部元素赋值从elements中赋值到新的元素数组中
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            //将新的元素数组赋值给array
            setArray(newElements);
        }
        //返回移除的元素
        return oldValue;
    } finally {
        //释放锁
        lock.unlock();
    }
}
//修改操作
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;
            //将新的元素数组赋值给array
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics:不是完全没有操作;确保易失性写入语义
            setArray(elements);
        }
        //返回索引的旧元素
        return oldValue;
    } finally {
        //释放锁
        lock.unlock();
    }
}

既然CopyOnWriteArrayList是线程安全的,那么在遍历数据的时候一定能遍历到最新的数据吗?

//迭代器遍历
public Iterator<E> iterator() {
    return new COWIterator<E>(getArray(), 0);
}
​
private COWIterator(Object[] elements, int initialCursor) {
    //cursor记录当前遍历到了哪个索引
    cursor = initialCursor;
    //将CopyOnWriteArrayList中的元素赋值给snapshot
    snapshot = elements;
}
//判断接下来是否还有数据
public boolean hasNext() {
    return cursor < snapshot.length;
}
public E next() {
    //如果没有的话就报错!
    if (! hasNext())
        throw new NoSuchElementException();
    //返回数据
    return (E) snapshot[cursor++];
}

通过上面的遍历器可以发现,CopyOnWriteArrayList类其实并不能够保证一定能够读取到最新的数据。因为迭代器获取的是array数组的内容,并且CopyOnWriteArrayList类每进行一次增删改的操作就会开辟一个新的内存空间来存储新的数据,所以它无法保证数据的实时一致性。

CopyOnWriteArrayList总结:

1、CopyOnWriteArrayList类中是通过Lock锁的方式来保证类中的一个安全性。

2、CopyOnWriteArrayList类不像AarrayList类一样固定扩容的一个时机和扩容的大小,CopyOnWriteArrayList每添加一个元素就会复制旧数组容量+1的一个新数组

3、CopyOnWriteArrayList类在增删改的方法中都是使用同一把锁,这样子就可以保证一个多线程环境中,增删改的方法不会相互影响

4、每进行一个增删改的操作就会开辟一个新的内存空间来存储元素,这样子也能够保证一定的线程安全。但是也消耗了内存空间

5、使用迭代器遍历数据的时候无法保证遍历数据的实时性。