ArrayList源码分析(二)

邮差的信 提交于 2019-12-15 12:35:41

ArrayList

ArrayList调整数组容量

默认扩容

每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

//grow是扩容方法,传入的参数为所需最小容量,通过判断,即可实现默认扩容为原来1.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);
}

手动扩容

默认扩容长度为原来的1.5倍,当需要添加数量比较大时,频繁扩容会影响效率,如果清楚知道业务数据量,可以使用ensureCapacity手动添加容量

手动扩容首先会判断数组是否有元素,需要容量minCapacity是否比默认容量DEFAULT_CAPACITY大,如果大才会触发扩容,否则就没有必要扩容。

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

ensureCapacityTest

long startTime = System.currentTimeMillis();
ArrayList<Object> list1 = new ArrayList<Object>();
for (int i = 1; i < 10000000; i++) {
    list1.add(new Object());
}
System.out.println(System.currentTimeMillis() - startTime);
//2098

long startTime2 = System.currentTimeMillis();
ArrayList<Object> list2 = new ArrayList<Object>();
list2.ensureCapacity(10000000);
for (int i = 1; i < 10000000; i++) {
    list2.add(new Object());
}
System.out.println(System.currentTimeMillis() - startTime2);
//237

trimToSize方法

使用trimToSize方法可以将底层数组的容量调整为当前列表保存的实际元素的大小

public void trimToSize() {
    //操作次数加一
    modCount++;
    //调整可能会出现两种情况,元素为空或是元素个数小于容量
    if (size < elementData.length) {
        elementData = (size == 0)
            ? EMPTY_ELEMENTDATA
            : Arrays.copyOf(elementData, size);
    }
}

fail-fast机制

fail-fast 机制是java集合(Collection)中的一种错误机制。当多个线程对同一个集合的内容进行操作时,就可能会产生fail-fast事件。
例如:当某一个线程A通过iterator去遍历某集合的过程中,若该集合的内容被其他线程所改变了;那么线程A访问集合时,就会抛出ConcurrentModificationException异常,产生fail-fast事件。

单线程环境下的fail-fast

public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    for (int i = 0 ; i < 10 ; i++ ) {
        list.add(i + "");
    }

    Iterator<String> iterator = list.iterator();
    int i = 0 ;
    while(iterator.hasNext()) {
        if (i == 3) {
                list.remove(3);
        }
        System.out.println(iterator.next());
        i ++;
    }
}

多线程环境下的fail-fast

public class FailFastTest {
     public static List<String> list = new ArrayList<>();

    //线程1读取
     private static class MyThread1 extends Thread {
           @Override
           public void run() {
                Iterator<String> iterator = list.iterator();
                while(iterator.hasNext()) {
                     String s = iterator.next();
                     System.out.println(this.getName() + ":" + s);
                     try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                }
                super.run();
           }
     }

    //线程2修改
     private static class MyThread2 extends Thread {
           int i = 0;
           @Override
           public void run() {
                while (i < 10) {
                     System.out.println("thread2:" + i);
                     if (i == 2) {
                           list.remove(i);
                     }
                     try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                     i ++;
                }
           }
     }

     public static void main(String[] args) {
           for(int i = 0 ; i < 10;i++){
            list.add(i+"");
        }
           MyThread1 thread1 = new MyThread1();
           MyThread2 thread2 = new MyThread2();
           thread1.setName("thread1");
           thread2.setName("thread2");
           thread1.start();
           thread2.start();
     }
}

iterator分析

当调用list.iterator()时,其源码是

public Iterator<E> iterator() {
    return new Itr();
}

即它会返回一个新的Itr类,而Itr类是ArrayList的内部类,实现了Iterator接口

>private class Itr implements Iterator<E>

三个属性:

    //集合遍历过程中的即将遍历的元素的索引
    int cursor;       // index of next element to return
    //记录刚刚遍历过的元素的索引,不存在上一个时为-1
    int lastRet = -1; // index of last element returned; -1 if no such
    //初始值就为ArrayList中的modCount
    int expectedModCount = modCount;

modCount是抽象类AbstractList中的变量,默认为0,而ArrayList 继承了AbstractList ,所以也有这个变量,modCount用于记录集合操作过程中作的修改次数,与size还是有区别的,并不一定等于size

public boolean hasNext() {
    //用cursor游标和size(集合中的元素数目)进行对比
    //相等时,表示遍历完成,返回false
    return cursor != size;
}

public E next() {
    //判断是否有其他线程修改过集合
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

final void checkForComodification() {
    //modCounut可能会改变
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

expectedModCount在整个迭代过程除了一开始赋予初始值modCount外,并没有再发生改变,所以可能发生改变的就只有modCount,可以知道在ArrayList进行add,remove,clear等涉及到修改集合中的元素个数的操作时,modCount就会发生改变(modCount ++),所以当另一个线程(并发修改)或者同一个线程遍历过程中,调用相关方法使集合的个数发生改变,就会使modCount发生变化,这样在checkForComodification方法中就会抛出ConcurrentModificationException异常。

避免fail-fast

方法1

在单线程的迭代器遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法。

方法2

使用java并发包(java.util.concurrent)中的类来代替ArrayList 和hashMap。
比如使用 CopyOnWriterArrayList代替ArrayList,CopyOnWriterArrayList在是使用上跟ArrayList几乎一样,CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,所以对于CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
对于HashMap,可以使用ConcurrentHashMap,ConcurrentHashMap采用了锁机制,是线程安全的。在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。即迭代不会发生fail-fast,但不保证获取的是最新的数据。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!