关键词:ArrayList CopyOnWriteArrayList 写时复制
ArrayList的线程不安全:
开启三个线程,每个线程分别向同一个ArrayList添加元素,最后查看ArrayList中元素的总个数。
(1)开启了三个线程,每个线程向同一个ArrayList中添加1000个元素(数量尽量可能大一些,这样不会让一个线程在一个时间片内就完成操作,从而让线程可以交替执行)
public static void main(String[] args) throws InterruptedException {
List<String> list = new ArrayList<>();
Thread threadA = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
list.add(i + "");
}
});
Thread threadB = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
list.add(i + "");
}
});
Thread threadC = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
list.add(i + "");
}
});
threadA.start();
threadB.start();
threadC.start();
threadA.join();
threadB.join();
threadC.join();
System.out.println("list size:" + list.size());
}
(2)输出结果:
截图选取了三个不同的运行结果,如果ArrayList是线程安全的,则最后的结果为:3000。但是从运行结果可以看出,显然ArrayList不是线程安全的。
(3)使用CopyOnWriteArrayList替换ArrayList
List<String> list = new CopyOnWriteArrayList<>();
多次反复运行,得到的结果都是:3000。可见CopyOnWriteArrayList是线程安全的。
█ CopyOnWriteArrayList
CopyOnWriteArrayList虽然名称中包含了“ArrayList”,但其并不是继承自ArrayList,而是和ArrayList一样都实现了List接口。其之所以名称中也使用“ArrayList”,是因为它和ArrayList一样都是基于数组实现的。
CopyOnWriteArrayList是线程安全的List,其使用加锁和写时复制来保证线程安全和效率。写时复制即对List进行写操作(添加或移除元素)的时候,将集合中的元素数组复制一份,并对该复制出来的数组进行写操作,写操作处理完成之后再将复制出来的数组替换到List实际存放元素的数组中。
当我们在编程中使用List集合时,通常有两个操作:将数据存放到集合(add)和获取集合中的数据(get)。对于add操作会改变集合的数据内容,而对于get是不会改变集合中的数据内容的,因此get是具有天然的线程安全的。既然get具有天然的线程安全,所以在get数据的时候不需要加锁操作也能保证线程安全。而add就需要加锁来保证线程线程了。CopyOnWriteArrayList的实现思想就是对集合的写操作(add、remove)进行加锁、复制新的数组操作,对读操作(get)不用加锁。
字段:
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
- lock
使用ReentrantLock对响应的操作进行加锁,保证线程安全。
- array
CopyOnWriteArrayList是基于数组实现的,数组array用来存放被添加到集合中的元素。CopyOnWriteArrayList中数组array的特点是数组的长度就是元素的个数,即数组中存放了几个元素,则创建的数组的长度就是几,不多不少,数组的长度按需创建。
构造器:
(1)无参构造器
初始化创建一个空的数组。
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
setArray:
final void setArray(Object[] a) {
// 将a数组赋值给array
array = a;
}
(2)有参构造器,参数为数组
使用Arrays.copyOf对参数数组内容进行拷贝到一个新的数组中,然后将新的数组赋值给array。
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
(3)有参构造器,参数为Collection集合
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
方法:
CopyOnWriteArrayList实现了List接口,在List中定义了add、addAll等插入元素的方法,remove、removeAll等移除元素的方法,retainAll、replaceAll等变更元素的方法、get、contains等查看元素的方法。下面主要从上面的几个方法来查看CopyOnWriteArrayList的实现。
在查看CopyOnWriteArrayList的方法时,主要要留意其在写操作(插入和移除元素)时,是如何体现“写时复制技术”的。还有读操作(查看)有什么特殊操作吗?
(1)写操作(插入、移除)
在查看这些方法的时候,注意到这些方法共有的操作是都使用了ReentrantLock互斥锁来保证同时只有一个线程能够执行操作,以此来保证线程安全。并且使用Arrays.copyOf或System.arraycopy来对数组的内容进行拷贝。
- add
public boolean add(E e) {
// 使用ReentrantLock保证线程安全
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取底层存放数据的数组
Object[] elements = getArray();
int len = elements.length;
// 对数组内容进行拷贝到一个新的数组中
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 将值添加到新的数组中
newElements[len] = e;
// 使用新的数组替换旧的数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
①ReentrantLock lock
使用ReentrantLock互斥锁保证每次同时只能有一个线程获取到锁,即只有一个线程能够执行add操作,以此来保证插入数据的线程安全。
②getArray
获取CopyOnWriteArrayList用来存放数据的底层数据结构-数组array
final Object[] getArray() {
return array;
}
③Arrays.copyOf
对数组array的内容进行拷贝,拷贝到一个新的数组中,新数组的长度比array数组的长度要多1一个长度,多一个长度就是用来存放新添加的元素的。Arrays提供了一个静态copyOf方法用来操作对数组的拷贝,使用到了System.arraycopy。
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
④newElements[len] = e
将添加的元素放进新数组中。
⑤setArray(newElements);
将array数组替换成新数组。
final void setArray(Object[] a) {
array = a;
}
- addAll
public boolean addAll(Collection<? extends E> c) {
Object[] cs = (c.getClass() == CopyOnWriteArrayList.class) ?
((CopyOnWriteArrayList<?>)c).getArray() : c.toArray();
if (cs.length == 0)
return false;
// 依然是使用了ReentrantLock互斥锁来保证线程安全
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// len==0表示目前的List集合为空,还没有任何元素
if (len == 0 && cs.getClass() == Object[].class)
setArray(cs);
else {
// 对array数组进行拷贝到新的数组中
Object[] newElements = Arrays.copyOf(elements, len + cs.length);
// 将集合c中的数据拷贝到新的数组中
System.arraycopy(cs, 0, newElements, len, cs.length);
setArray(newElements);
}
return true;
} finally {
lock.unlock();
}
}
①Object[] newElements = Arrays.copyOf(elements, len + cs.length);
对CopyOnWriteArrayList的存放数据的数组array内容进行拷贝到新的数组中。
②System.arraycopy(cs, 0, newElements, len, cs.length);
对参数的集合c所对应的数组内容拷贝到新的数组中。
- remove
remove方法具有两个重载方法,分别为:
E remove(int index);
boolean remove(Object o);
remove(int index):
public E remove(int index) {
// 使用ReentrantLock加锁来保证线程安全
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();
}
}
①get(elements, index)
获取数组中对应下标的值。
private E get(Object[] a, int index) {
return (E) a[index];
}
②int numMoved = len - index - 1;
当numMoved==0时,表示index是数组的最后一个元素的下标,此时移除的是数组的最后一个元素。既然是移除的最后一个元素,则数组的最后一个空间就多余了,此时要进行缩容。setArray(Arrays.copyOf(elements, len - 1));即对数组进行缩容。
③ Object[] newElements = new Object[len - 1];
当移除的元素不是数组最后的元素,而是中间元素,就需要如下的操作了。
创建一个新的数组为数组内容拷贝做准备,新数组的长度比旧数组的长度要少1,因为要移除一个元素了嘛,自然就需要少一个长度了。
④System.arraycopy(elements, 0, newElements, 0, index);
将数组array中0到index位置的数据先拷贝到新数组中。
⑤System.arraycopy(elements, index + 1, newElements, index,
numMoved);
再将数组array中index+1到最后的数据拷贝到新数组中。经过上面两个步骤,也就是除了index位置的数据不拷贝外,其他位置的数据都要拷贝到新数组中。
remove(Object o):
public boolean remove(Object o) {
Object[] snapshot = getArray();
int index = indexOf(o, snapshot, 0, snapshot.length);
return (index < 0) ? false : remove(o, snapshot, index);
}
①indexOf
获取参数o在数组中下标的位置。实现方式就是去遍历数组,一个一个元素进行equals比较,匹配到就返回对应的下标,否则返回-1。
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
②index < 0
index<0,即index=-1。表明数据o在数组中不存在。
③remove
根据下标移除对应位置的数据。该方法具体的实现逻辑就不分析了,主要要留意到其也是使用了ReentrantLock来保证线程安全,以及对数组内容进行了拷贝。
private boolean remove(Object o, Object[] snapshot, int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] current = getArray();
int len = current.length;
if (snapshot != current) findIndex: {
int prefix = Math.min(index, len);
for (int i = 0; i < prefix; i++) {
if (current[i] != snapshot[i] && eq(o, current[i])) {
index = i;
break findIndex;
}
}
if (index >= len)
return false;
if (current[index] == o)
break findIndex;
index = indexOf(o, current, index, len);
if (index < 0)
return false;
}
Object[] newElements = new Object[len - 1];
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
在所有写操作方法中使用的是同一个ReentrantLock对象:ReentrantLock lock = this.lock; 因为所以的这类方法同时只能有一个线程获取到锁然后执行对应方法的逻辑,而执行其他方法的线程需要等待锁的时候然后获取到锁才能继续执行。即有一个线程调用了add方法向集合中添加元素,在add方法没有执行完成释放锁之前,另外一个线程调用了remove方法去移除集合中的元素,则此时该线程就会被阻塞等待,remove方法的逻辑无法继续向下执行,只有等执行add方法的线程释放了锁,而且执行remove方法的线程获取到了锁,remove方法才会继续向下执行。
从上面写操作的代码中可以看出,所有的写操作都是将原先旧的数组内容进行复制到一个新的数组中,然后对新的数组进行操作,最后将新的数组赋值给array字段。所以每次经过写操作,array指向的都是不同的数组对象。
(2)读操作(获取元素)
读操作具有天然的线程安全,因此不需要加锁来控制。
- get
就是获取数组array,然后取下标的值。没有加锁,没有数组拷贝。
public E get(int index) {
return get(getArray(), index);
}
CopyOnWriteArrayList还有其他方法,在这里就不介绍了,感兴趣的同学可自行查看jdk源码。