文章目录
前言
日常开发时,我们使用使用ArrayList自然是线程不安全的,这个是众所周知的,因为常规下线程操作或者业务代码是不会涉及多线程操作单集合的高并发场景的。
一旦在高并发对某个集合需要进行又读又写的情况时,还有人敢用ArrayList嘛?
vector synchronizedList真的线程安全嘛?
源码解读以及代码演示错误
对于上述情况,可能很多人都会想到使用vector,确实,如下图所示,vector代码中增删改查都带有synchronized
Collections.synchronizedList同理只是锁粒度不同而已
但是他们真的安全嘛?如下代码所示,在遍历时进行集合清除操作,就会出现异常。
@Test
public void testVectorConcurrentReadWrite() {
Vector<Integer> vector = new Vector<>();
vector.add(1);
vector.add(2);
vector.add(3);
vector.add(4);
vector.add(5);
for (Integer item : vector) {
new Thread(vector::clear).start();
System.out.println(item);
}
}
为什么加了synchronized之后在多线程操作下还会出现异常呢?
我们以上述示例代码vector为例,定位到他的异常,如下图所示,我们可以看到这样一个异常,从源码我们可以就有这样一个疑问,为什么modCount != expectedModCount
就会报异常呢?
通过通读vector源码我们可以知道当对集合进行修改操作时,都会对modCount ,进行自增,如下图所示
当对集合进行遍历的时候,我们会创建一个全新的迭代器对象,如下图所示,这个迭代器的expectedModCount初始化的值与modCount 相等。
了解这些我们就能很好的解释了为什么之前的代码会报错,在多线程情况下,一个线程进行添加,不断修改当前集合的
modCount
,在这期间,另一个线程初始化一个迭代器进行遍历,expectedModCount
会初始化为第一个线程某个阶段的modCount
,导致这两个值不相等就出现之前的错误了。
cow思想,高并发线程安全的最佳解决方案
简述
这时候我们就提出一种不错的解决方案,cow思想,即在读的时候使用原来了的集合对象,当需要对集合进行写操作时,我们就创建一个新的对象,提供的这个需要进行修改操作的线程,当所有迭代完成后,再将旧集合的指针指向修改后的新集合。
解读CopyOnWriteArrayList源码
如下所示,当需要修改操作时就加锁,创建一个新集合,添加一个新元素,再将原集合指针指向新指针,从而完成写操作。
而读操作自然只要拿到原集合即可。
与传统集合的比对
与传统集合相比,CopyOnWriteArrayList更适合读多写少的情况,例如:黑名单、配置等相关集合。如下代码所示,我们就能看出写操作CopyOnWriteArrayList确实开销更大。且CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。这时候就会可能会出现数据一致性问题。
public static void main(String[] args) {
long start = System.currentTimeMillis();
List<Integer> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
int loopCount = 100000;
for (int i = 0; i < loopCount; i++) {
copyOnWriteArrayList.add(1);
}
long end = System.currentTimeMillis();
System.out.println(end - start);
start = System.currentTimeMillis();
List<Integer> synchronizedList = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < loopCount; i++) {
synchronizedList.add(1);
}
end = System.currentTimeMillis();
System.out.println(end - start);
}