[Java] ArrayList 扩容原理和线程不安全
基础知识
ArrayList 内部维护一个数组,这个数组是一个 Object 类型的数组,可以存储任何类型的对象。当你向 ArrayList 中添加元素时,元素被存储在这个数组中。
当添加元素时,它会检查当前元素数量是否已经达到了内部数组的容量限制。如果达到了限制,ArrayList 会创建一个新的数组,通常是当前容量的 1.5 倍(具体增长因实现而异),然后将现有元素从旧数组复制到新数组。
ArrayList 在插入和删除元素方面的性能比较差。如果你在中间位置插入或删除元素,需要将后续元素向后或向前移动,以维护连续的元素存储。这些操作的时间复杂度是线性的(O(n)
),其中 n 是数组中的元素数量。
分析扩容步骤
file:[Main.java]
public class Main {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(0);
list.add(1);
list.add(2);
list.add(3);
System.out.println(list);
}
}
如上所示,new 一个 ArrayList,调用无参构造函数,创建一个空的数组,数组长度为 0:
file:[ArrayList.java]
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
后续调用了 4 次 add
方法,往 ArrayList 中添加元素。添加时会涉及到四个方法:
file:[ArrayList.java]
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
private Object[] grow() {
return grow(size + 1);
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
依次从下往上的顺序调用这四个方法,首先检查当前 size
ArrayList 的长度是否等于真实存储数据的数组(elementData)长度一致,如果长度一致就说明数组已经达到了最大容量,就需要调用 grow()
方法进行扩容。
tip:[start]
elementData
是 ArrayList 内部维护的真实存储数据的数组,其类型为 Object。
tip:[end]
如果是初始化之后添加元素,会走 else
分支,为其创建一个长度为 10 的数组,数组长度由常量 DEFAULT_CAPACITY
指定。然后让 elementData
指向一个新的数组。此时的 elementData
已经是一个长度为 10 的数组了,然后回到 add(E e, Object[] elementData, int s)
方法,在数组的最后一个位置上面添加元素,此时的数组长度 size = s + 1;
,自加 1。
否则,就走 if
分支,计算对 elementData
数组扩容多长,然后调用 Arrays.copyOf
把以前数组里面的内容复制到新的数组里,并让 elementData
指向一个新的数组。
tip:[start]
建议复制代码到 Idea 进行调试查看初始化和扩容的步骤。给 new、和 add 这些地方添加上断点,然后按 Ctrl+F5
进行调试。
tip:[end]
线程不安全
ArrayList 是一个线程不安全的集合,也就是说在多线程、并发的业务场景下,操作 ArrayList 是不安全的,会有数据不一致的情况。线程安全的 List 实现类是 Vector
,它们很相似,但是是线程安全的。
如果非要用 ArrayList,可以通过 Collections.synchronizedList(new ArrayList())
创建它。
file:[Main.java]
public class Main {
public static void main(String[] args) {
ArrayList<Integer> arrayList = new ArrayList<>();
final List<Integer> synchronizedList = Collections.synchronizedList(arrayList);
Runnable addTask = () -> {
for (int i = 0; i < 1000; i++) {
synchronizedList.add(i);
}
};
Runnable removeTask = () -> {
for (int i = 0; i < 1000; i++) {
if (!synchronizedList.isEmpty()) {
synchronizedList.remove(synchronizedList.size() - 1);
}
}
};
Thread thread1 = new Thread(addTask);
Thread thread2 = new Thread(removeTask);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.fillInStackTrace();
}
System.out.println("ArrayList size: " + synchronizedList.size());
}
}
通过线程安全的创建 ArrayList,在多线程的情况下,不会抛出异常,并且数据也是一致性的。