Java常用集合类之ArrayList&CopyOnWriteArrayList

ArrayList是单线程的数据结构,在多线程环境中容易发生不可预知的错误。因此Java类库为我们提供了CopyOnWriteArrayList在多线程中使用。

先来看看ArrayList,它有以下属性


    private static final int DEFAULT_CAPACITY = 10;

    private static final Object[] EMPTY_ELEMENTDATA = {};

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    transient Object[] elementData; // non-private to simplify nested class access

    private int size;
    
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
  • elementData:类型是transient Object[],说明ArrayList底层存储数据的方式还是数组,只不过ArrayList可以动态改变数组的大小(当然数组本身的大小在声明后就不能改变,ArrayList这里是用了创建新数组的方式改变它的容量)。至于为什么使用transient,可以参考这篇博文
    ArrayList中elementData为什么被transient修饰?
  • EMPTY_ELEMENTDATA:当使用new ArryaList(0)创建一个空ArrayList时elementData = EMPTY_ELEMENTDATA,如果程序中有多个空ArrayList,那么它们都会指向同一个EMPTY_ELEMENTDATA,这样程序就得到了优化。
  • DEFAULTCAPACITY_EMPTY_ELEMENTDATA:当使用new ArrayList()创建一个空ArrayList时elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA,然后向数组中添加第一个元素时elementData大小会扩展为DEFAULT_CAPACITY

上述两者的区别是在java1.8才有的。通过ArrayList的构造函数可以很清楚的知道它们两者使用的区别

public ArrayList(int initialCapacity) {
     if (initialCapacity > 0) {
           this.elementData = new Object[initialCapacity];
       } else if (initialCapacity == 0) {
           this.elementData = EMPTY_ELEMENTDATA;
       } else {
           throw new IllegalArgumentException("Illegal Capacity: "+
                                              initialCapacity);
       }
   }
public ArrayList() {
     this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
   }
  • DEFAULT_CAPACITY:ArrayList默认容量
  • size:ArrayList大小(含有元素的个数),由于没有声明大小,默认为0。注意这里的size和elementData.length不一样,size指的是数组中的元素个数,而elementDate.length是数组长度。数组末尾可能会空出来因此elementData.length>size。
  • MAX_ARRAY_SIZE:ArrayList的最大容量,为Integer.MAX_VALUE - 8

那add()方法是如何让一个空数组大小变为DEFAULT_CAPACITY的呢?先来看看add()方法

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

其中调用了ensureCapacityInternal()方法,这个方法又使用了一套组合拳,如下:

//组合拳入口,用于确保elementData.length > size,否则调用grow方法扩容
private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
/**
* modCount是AbstractList中的属性,用于记录ArrayList被修改的次数,用于确保同一时间只能有一个线程对
* 它进行操作。
*
*/
private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

//一次增加0.5倍
private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

其他的方法没什么比较特别的就不多讲,接下来再看看CopyOnWriteArrayList,看看它使用了什么机制能够在多线程环境中使用。

CopyOnWriteArrayList有以下属性:


final transient ReentrantLock lock = new ReentrantLock();

private transient volatile Object[] array;

private static final sun.misc.Unsafe UNSAFE;

private static final long lockOffset;

lock:用于同步
array:底层存放数据的地方
UNSAFE和lockOffset用于对lock在反序列化是进行重新设置。因为lock是transient类型的,是不会进行序列化的。
为什么CopyOnWriteArrayList没有size属性?我们可以看看它的add()方法:

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;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

1.获取锁,进行加锁
2.获取原数组及其长度,并拷贝到新数组中,最后在将新数组最后一个元素设置为要添加的元素。在 将新数组替换原数组
3.释放锁

可以看出上述操作在每次add()后数组大小只增加一,并没有像ArrayList一次增加多个。因此数组的size就等于array.length。

再来看看修改操作:

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();
        }
    }

1.获取锁,加锁
2.获取原数组和索引对应的元素
3.将要修改的元素和传入的元素相比较,如果相等,就将原数组重新设置为当前数组
4.如果不相等,将原数组复制到新数组,再在新数组索引处修改元素的值,再将新数组替换原数组
5.释放锁

有同学可能会问,为什么获取array和设置array要通过getArray()和setArray()呢?不能直接对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;
    }

原因其实很简单,因为array是私有属性,为了能够实现array在concurrent包里能够访问,只能通过getArray()和setArray(),又为了编码风格一致,所以在该类里也使用这两个函数。

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