1 概述
我们来设想一个场景,在并发的情况下,我们想要使用一个List来保存对象,这个时候为了让List是线程安全的,我们能怎么做呢?首先我们能够想到的应该是使用synchronized对List对象加锁,或者在使用List对象的地方使用ReentrantLock来保证线程同步,但是这样就有一个问题,我们知道对List中的对象进行读操作的时候并不会存在数据一致性的问题,只有在写的时候才有数据一致性的问题,所以这个时候就需要我们的CopyOnWriteArrayList上场了。
CopyOnWriteArrayList使用了读写锁分离的思想,针对写操作重写复制一份数据,将新数据添加到复制的数据当中,然后将对象的引用指向新的集合。而在读取操作的时候,直接读取集合本身的数据,而不需要复制操作,下面我们来看一个CopyOnWriteArrayList使用的具体实例。
2 使用实例
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author: LIUTAO
* @Date: Created in 2019/2/22 14:02
* @Modified By:
*/
public class CopyOnWriteArrayListDemo {
public static void main(String[] args) {
List<String> a = new ArrayList<>();
a.add("a");
a.add("b");
a.add("c");
CopyOnWriteArrayList<String> b = new CopyOnWriteArrayList<>(a);
Thread thread = new Thread(() ->{
int count = 1;
while (count < 5){
b.add(String.valueOf(count++));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.setDaemon(true);
thread.start();
b.stream().forEach( str -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(str);
System.out.println(b.hashCode());
});
}
}
程序输出如下:
a
121226914
b
534948995
c
534948995
从上面的输出我们可以看出b的引用地址在随着向里面写入数据而改变。所以这也验证了CoypOnWriteArrayList针对写如操作是将数据进行复制后写入,然后将集合地址重新指向新的地址。
在这里我们顺便说一下CoypOnWriteArrayList的使用场景为读多写少的场景,比如黑名单、白名单和导航菜单。
3 源码分析
针对CoypOnWriteArrayList的源码,我们仅仅分析下add和get函数就行,其余的函数大家可以自行查看源码,也不是很难。
(1)add
前文我们提到了CoypOnWriteArrayList的写入操作是进行了线程同步操作的,并且将原数据复制一份,然后在副本中进行数据操作后,将集合的指针指向副本。下面我们来看看具体的逻辑。
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来保证线程安全。
(2)get
通过对ArrayList源码的分析,我们纸袋ArrayList底层是使用数组来保存数据的。所以从CoypOnWriteArrayList的名字我们可以猜想它也是使用数组来保存数据的。
public E get(int index) {
return get(getArray(), index);
}
//获取数据数组
final Object[] getArray() {
return array;
}
//根据索引返回数组的数据
private E get(Object[] a, int index) {
return (E) a[index];
}
上面就是对源码的分析,欢迎大家交流和指正。谢谢!