ArrayList和CopyOnWriteArrayList详解和源码剖析【扩容机制】

ArrayList

//默认初始容量。
private static final int DEFAULT_CAPACITY = 10;
//空数组实例。
private static final Object[] EMPTY_ELEMENTDATA = {};
//用于 默认大小的 空数组实例。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//存储ArrayList元素的数组缓冲区
transient Object[] elementData;
//要分配的数组的最大大小(除非必要)
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

ArrayList(int initialCapacity)

构造一个具有指定初始容量的空列表。以无参数构造方法创建 ArrayList 时,实际上初始化赋值的是 一个空数组。当真正对数组进行添加元素操作时,才真正分配容量。即向数组中添加 第一个元素时,数组容量扩为 10

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

JDK7 new无参构造的ArrayList对象时,直接创建了长度是10的Object[]数组elementData 。jdk7中的ArrayList的对象的创建类似于单例的饿汉式,而jdk8中的ArrayList的对象的创建类似于单例的懒汉式。JDK8的内存优化也值得我们在平时开发中学习。

ArrayList()

构造一个初始容量为 0 的空列表。

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

ArrayList(Collection<? extends E> c)

构造一个包含指定集合元素的列表,按照集合的迭代器返回的顺序。

public ArrayList(Collection<? extends E> c) {
    Object[] a = c.toArray();	//转为数组
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            elementData = a;	//是ArrayList类型,直接赋值
        } else {	//否则将非Obj类型的数组赋值给新的Obj类型的数组
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        //空数组
        elementData = EMPTY_ELEMENTDATA;
    }
}

trimToSize

将此 ArrayList 实例的容量修剪为列表的当前大小。应用程序可以使用此操作来最小化 ArrayList 实例的存储。

public void trimToSize() {
    modCount++;
    if (size < elementData.length) {
        elementData = (size == 0)
            ? EMPTY_ELEMENTDATA
            : Arrays.copyOf(elementData, size);
    }
}

indexOf

public int indexOf(Object o) {		//lastIndexOf
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

clone

返回此 ArrayList 实例的浅表副本。 (元素本身不会被复制。)

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

toArray

以正确的顺序(从第一个元素到最后一个元素)返回一个包含此列表中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。如果列表适合指定的数组(a.length < size),则在其中返回。否则,将使用指定数组的运行时类型(a)和此列表的大小(size)分配一个新数组。

如果列表适合指定的数组并有剩余空间(即,数组的元素多于列表),则数组中紧跟集合末尾的元素设置为 null。 (仅当调用者知道列表不包含任何空元素时,这对确定列表的长度很有用。)

public <T> T[] toArray(T[] a) {
    if (a.length < size)
        return (T[]) Arrays.copyOf(elementData, size, a.getClass());
    System.arraycopy(elementData, 0, a, 0, size);
    
    if (a.length > size)
        a[size] = null;
    return a;
}

grow(int minCapacity)

//增加容量以确保它至少可以容纳最小容量参数指定的元素数量。
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    //将oldCapacity 右移一位,其效果相当于oldCapacity / 2
    //oldCapacity 为偶数就是 1.5 倍,为奇数就是 1.5 倍左右
    //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来
    // 比较 minCapacity 和最大容量 MAX_ARRAY_SIZE
    // 如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`
    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);
}

hugeCapacity

主要用于处理新容量比最大容量闲置还大的情况,也就是巨大容量。

private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0) // overflow
        throw new OutOfMemoryError();
    return (minCapacity > MAX_ARRAY_SIZE)
        ? Integer.MAX_VALUE	//要求的容量大于最大限制容量,直接返回Integer.MAX_VALUE
        : MAX_ARRAY_SIZE;	//否则返回最大限制容量
}

从上面 grow() 方法源码我们知道: 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) hugeCapacity() 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,如果 minCapacity 大于最大容量,则新容量则为 Integer.MAX_VALUE ,否则,新容量大小则为MAX_ARRAY_SIZE 即为 Integer.MAX_VALUE - 8 。

add(int index, E element)

在此列表中的指定位置插入指定的元素。 先调用 rangeCheckForAdd 对index进行界限检查;然后调用 ensureCapacityInternal 再将从index开始之后的所有成员后移一个位置;将element插入index位置;最后size加1。

public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //arraycopy()方法实现数组自己复制自己
    System.arraycopy(elementData, index, elementData, index + 1,size - index);
    //elementData:源数组;index:源数组中的起始位置;elementData:目标数组;
    elementData[index] = element;
    size++;
}

add(E e)

将指定元素附加到此列表的末尾

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

注意 :JDK11 移除了 ensureCapacityInternal() 和 ensureExplicitCapacity() 方法

ensureCapacityInternal

//得到最小扩容量
private void ensureCapacityInternal(int minCapacity) {
    // 获取默认的容量和传入参数的较大值
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

当 要 add 进第 1 个元素时,minCapacity 为 1,在 Math.max()方法比较后,minCapacity 为 10

ensureExplicitCapacity

//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        //调用grow方法进行扩容,调用此方法代表已经开始扩容了
        grow(minCapacity);
}

当我们要 add 进第 1 个元素到 ArrayList 时,elementData.length 为 0 (因为还是一个空的 list),因为执行了 ensureCapacityInternal() 方法 ,所以 minCapacity此时为 10。此时, minCapacity - elementData.length > 0 成立,所以会进入grow(minCapacity) 方法。

当 add 第 2 个元素时,minCapacity 为 2,此时 e lementData.length(容量)在添加第一个元素后扩容成 10 了。此时, minCapacity - elementData.length > 0 不成立,所以不会进入 (执行) grow(minCapacity) 方法。

添加第 3、4···到第 10 个元素时,依然不会执行 grow 方法,数组容量都为 10。在此列表中的指定位置插入指定元素。将当前位于该位置的元素(如果有)和任何后续元素向右移动(将其索引加一)。

直到添加第 11 个元素,minCapacity(为 11)比 elementData.length(为 10)要大。进入grow 方法进行扩容。

Arrays.copyOf()

个人觉得使用 Arrays.copyOf() 方法主要是为了给原有数组扩容。复制指定的数组,用零截断或填充(如有必要),以便副本具有指定的长度。对于在原始数组和副本中都有效的所有索引,这两个数组将包含相同的值。对于在副本中有效但在原始数组中无效的任何索引,副本将包含 0。当且仅当指定长度大于原始数组的长度时,此类索引才会存在。

Arrays的copyOf()方法传回的数组是新的数组对象,改变传回数组中的元素值,不会影响原来的数组。copyOf()的第二个自变量指定要建立的新数组长度,如果新数组的长度超过原数组的长度,则保留数组默认值。

public static int[] copyOf(int[] original, int newLength) {
    int[] copy = new int[newLength];
    System.arraycopy(original, 0, copy, 0,
                     Math.min(original.length, newLength));
    return copy;
}

System.arraycopy()

// 我们发现 arraycopy 是一个 native 方法,接下来我们解释一下各个参数的具体意义 
/** * 复制数组 
* @param src 源数组 
* @param srcPos 源数组中的起始位置 
* @param dest 目标数组 
* @param destPos 目标数组中的起始位置 
* @param length 要复制的数组元素的数量
*/
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

两者联系和区别

联系:看两者源代码可以发现 copyOf() 内部实际调用了 System.arraycopy() 方法

区别: arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置。 copyOf() 是系统自动在内部新建一个数组,并返回该数组。

ensureCapacity

如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳最小容量参数指定的元素数量。
最好在 add 大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数
测试:在N很大的时候,使用 list.ensureCapacity(N); 比不使用的总add时间短了很多。

public void ensureCapacity(int minCapacity) {
    int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
        // any size if not default element table
        ? 0
        // larger than default for default empty table. It's already
        // supposed to be at default size.
        : DEFAULT_CAPACITY;

    if (minCapacity > minExpand) {
        ensureExplicitCapacity(minCapacity);
    }
}

remove

从此列表中删除第一次出现的指定元素(如果存在)。如果列表不包含该元素,则它不变。更正式地说,删除具有最低索引 i 的元素,例如 Objects.equals(o, get(i)) 。

public boolean remove(Object o) {
    final Object[] es = elementData;
    final int size = this.size;
    int i = 0;
    found: {
        if (o == null) {
            for (; i < size; i++)
                if (es[i] == null)
                    break found;
        } else {
            for (; i < size; i++)
                if (o.equals(es[i]))
                    break found;
        }
        return false;
    }
    fastRemove(es, i);
    return true;
}

跳过边界检查且不返回已删除值的私有删除方法。

private void fastRemove(Object[] es, int i) {
    modCount++;
    final int newSize;
    if ((newSize = size - 1) > i)
        System.arraycopy(es, i + 1, es, i, newSize - i);
    es[size = newSize] = null;
}

CopyAndWriteArrayList

CopyOnWrite的应用场景

和ArrayList最大的不同的是,CopyAndWriteArrayList不需要扩容,且CopyOnWrite并发容器用于读多写少的并发场景。

比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。

这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。

CopyOnWriteArrayList的缺点

CopyOnWrite容器有很多优点(解决开发工作中的多线程的并发问题),但是同时也存在两个问题,即内存占用问题和数据一致性问题。

1.内存占用问题

因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。
如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC

针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap

2.数据一致性问题。

CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

final transient ReentrantLock lock = new ReentrantLock();	//可重入锁,保证线程安全
private transient volatile Object[] array;		//每次复制后都会赋值给这个array

构造方法

构造方法有三个,基本和ArrayList的一样,不涉及到加锁。

add

基本原理:
  初始化的时候只有一个容器,很常一段时间,这个容器数据、数量等没有发生变化的时候,大家(多个线程),都是读取(假设这段时间里只发生读取的操作)同一个容器中的数据,所以这样大家读到的数据都是唯一、一致、安全的,但是后来有人往里面增加了一个数据,这个时候CopyOnWriteArrayList 底层实现添加的原理是先copy出一个容器(可以简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给了之前那个旧的的容器地址,但是在添加这个数据的期间,其他线程如果要去读取数据,仍然是读取到旧的容器里的数据

CopyOnWriteArrayList中add方法的实现(向CopyOnWriteArrayList里添加元素),可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。

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;
        if (numMoved == 0)
            newElements = Arrays.copyOf(elements, len + 1);		//copy出新的副本出来
        else {
            newElements = new Object[len + 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index, newElements, index + 1,
                             numMoved);
        }
        newElements[index] = element;
        setArray(newElements);		//将新的引用地址赋值给旧的引用地址
    } finally {
        lock.unlock();
    }
}

get

读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。

private E get(Object[] a, int index) {
    return (E) a[index];
}

remove

删除元素,很简单,就是判断要删除的元素是否最后一个,如果最后一个直接在复制副本数组的时候,复制长度为旧数组的 length-1 即可;
但是如果不是最后一个元素,就先复制旧的数组的index前面元素到新数组中,然后再复制旧数组中index后面的元素到数组中,最后再把新数组的引用赋值给旧数组的引用。最后在finally语句块中将锁释放。

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 {		//如果不是最后一个元素
            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();
    }
}
  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Charte

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值