ArrayList、CopyOnWriteArrayList源码剖析

上一期我们探究了ConcurrentHashMap,今天我们来探究ArrayList和CopyOnWriteArrayList。

ArrayList和HashMap一样,是我们在java编程中经常使用的一种数据结构,底层实现基于数组,并且可以自动进行扩容操作。允许 null 的存在,实现了RandomAccess、Cloneable、Serializable 接口,所以ArrayList 是支持快速访问、复制、序列化。由于基于数组实现,因此,get方法的时间复杂度为O(1),而add方法的时间复杂度为O(n),这是基于数组的特性(查找快,增删慢)决定的。和HashMap一样,有fail-fast机制,也是线程不安全的,多线程环境下会抛出ConcurrentModificationException的异常。

一、ArrayList的相关字段

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * 默认的容量
     */
    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;
}

除了上述字段外,在AbstractList抽象类中还有一个modCount,和HashMap一样,用于记录ArrayList修改的次数。

二、ArrayList的一些方法

①我们先来看一下ArrayList的两个构造方法,其中一个是指定容量,一个是空构造器:

/**
 * 指定容量大小的构造方法
 */
public ArrayList(int initialCapacity) {
    //如果指定容量大于0,则进行new一个Object数组出来
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    //如果指定容量为0,则把EMPTY_ELEMENTDATA赋值给elementData
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    }
}

/**
 * 假如我们调用空参构造,是把
 * DEFAULTCAPACITY_EMPTY_ELEMENTDATA赋给了elementData
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

②add()方法

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

首先,先确保数组容量是否满足,满足则直接给数组赋值,不满足则扩容后赋值。

③ensureCapacityInternal方法

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;
    //此时所需容量 大于 数组的长度则需要扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //假如我们当时使用的是空参构造,则开始elementData为
    //DEFAULTCAPACITY_EMPTY_ELEMENTDATA,首次添加会给容量赋默认值
    //DEFAULT_CAPACITY = 10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

④grow()方法

private void grow(int minCapacity) {
    //拿到旧容量
    int oldCapacity = elementData.length;
    //位运算扩容到1.5倍
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //如果扩容为1.5倍仍然不够,扩容为所需最小容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //若新容量比数组承受最大的容量还要大
    //则再到MAX_ARRAY_SIZE或者Integer.MAX_VALUE
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    //将旧数组的元素都copy到新数组
    elementData = Arrays.copyOf(elementData, newCapacity);
}

上述方法就是给ArrayList的添加和扩容的方法,但是当我们Remove时不会进行自动的缩容操作,因为ArrayList删除很慢,频繁删除建议使用LinkedList。在ArrayList中也提供了缩容方法trimToSize(),但是不会自己主动调用。

三、ArrayList的一些问题

开头我们说了ArrayList是线程不安全的,当多个线程同时添加元素时就会抛出异常。

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
    for (int i = 0; i < 3; i++) {
        new Thread(() -> {
            for (int j = 0; j < 10; j++) {
                list.add(j);
            }
            System.out.println(Thread.currentThread().getName() + "\t" + list);
        }).start();
    }
}

上述Demo创建了3个线程分别向list中添加10个元素,然后查看list中的元素,就会抛出异常

Exceptthread "Thread-1" Exception in thread "Thread-0" java.util.ConcurrentModificationException
 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
 at java.util.ArrayList$Itr.next(ArrayList.java:861)
 at java.util.AbstractCollection.toString(AbstractCollection.java:461)
 at java.lang.String.valueOf(String.java:2994)
 at java.lang.StringBuilder.append(StringBuilder.java:131)
 at com.study.list.ArrayListDemo.lambda$main$0(ArrayListDemo.java:18)
 at java.lang.Thread.run(Thread.java:748)
java.util.ConcurrentModificationException
 at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
 at java.util.ArrayList$Itr.next(ArrayList.java:861)
 at java.util.AbstractCollection.toString(AbstractCollection.java:461)
 at java.lang.String.valueOf(String.java:2994)
 at java.lang.StringBuilder.append(StringBuilder.java:131)
 at com.study.list.ArrayListDemo.lambda$main$0(ArrayListDemo.java:18)
 at java.lang.Thread.run(Thread.java:748)
Thread-2 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]ion in

那怎么解决呢?

①使用Collections.synchronizedList()方法,效率太低,所有方法内部是synchronize关键字修饰的同步代码块。不能共享读

②使用Vector,效率依旧很低,全是同步方法。

那咋办呢?于是乎,CopyOnWriteArrayList应运而生。

四、CopyOnWriteArrayList简介

CopyOnWriteArrayList,顾名思义,写时复制,和读写锁类似,但是CopyOnWriteArrayList只有写写互斥,读读、读写、写读均不互斥。

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

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

在CopyOnWriteArrayList中,使用到了ReentrantLock,目的是为了保证只有一个线程在进行写操作,实现写写互斥。volatile修饰的array,保证了该字段的可见性。

五、CopyOnWriteArrayList的一些方法

首先我们来看一下构造方法

/**
 * array的get方法.
 */
final Object[] getArray() {
    return array;
}

/**
 * array的set方法.
 */
final void setArray(Object[] a) {
    array = a;
}

/**
 * 空构造器,默认设置一个空Object数组
 */
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}

/**
 * 传入一个Collection的构造器
 */
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    //如果本身就是CopyOnWriteArrayList,直接调用get方法
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    else {
        //将元素copy到新数组中
        elements = c.toArray();
        if (c.getClass() != ArrayList.class)
            elements = Arrays.copyOf(elements, elements.length, Object[].class);
    }
    //调用set方法赋值
    setArray(elements);
}

/**
 * 传入一个数组
 */
public CopyOnWriteArrayList(E[] toCopyIn) {
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}

构造方法的核心就是给字段array赋值。

①get()

@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
    return (E) a[index];
}
public E get(int index) {
    return get(getArray(), index);
}

get方法基本没什么可说的,就是返回数组下标的位置的元素。

②add()

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    //上锁,保证只有一个线程可以进行写操作
    lock.lock();
    try {
        //获取旧数组
        Object[] elements = getArray();
        //获取旧数组长度
        int len = elements.length;
        //将旧数组copy到新数组,并且长度扩充1
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //将新添加的元素放到末尾
        newElements[len] = e;
        //将新数组的赋值给array字段
        setArray(newElements);
        return true;
    } finally {
        //解锁
        lock.unlock();
    }
}

这个add方法就比较牛了,首先他使用了ReetrantLock保证了写操作只有一个线程进入,其他的都必须等待直到获取到锁。当每次写入时,都会将旧数组Copy一份,然后长度+1,放到末尾后再返回新数组。大概就是下图这个样子:

图片

我们假设现在要添加一个"吴八",此时线程二进行写入,会先copy一份出来,然后长度增加1,把新元素放进去,假如再没有返回新数组引用之前,线程一读,读的还是之前的旧数组,从而保证了读写可以共存。

今天的分享就到此结束啦,喜欢的小伙伴记得点赞呦。

下期预告:Synchronize底层究竟是什么?

关注公众号JavaGrowUp,下期不迷路,获取更多精彩内容。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值