上一期我们探究了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,下期不迷路,获取更多精彩内容。