1.ArrayList
1.1前言
在高并发情况下,java中原先的集合就会出现各种各样大的问题
ArrayList 所有的方法都是非线程安全的
从上图中的ArrayList
源码可知,该类里面所有的方法都是未加锁的,因此在高并发的情况下线程安全问题。一般只是在单线程中使用该类。
1.2 问题演示
代码如下:
import java.util.ArrayList;
import java.util.List;
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for(int i = 1; i <= 3; i++) {
new Thread(() -> {
list.add(String.valueOf(Math.random()).substring(0,4));
System.out.println(Thread.currentThread().getName() + ":" + list);
},String.valueOf(i)).start();
}
}
}
上述代码中利用循环创建了三个线程,并且都往同一个List
中添加数据。运行结果如下:
多运行几次结果如下所示
基本上每次结果都大体不同
当修改创建的线程数由3个变成40个时还会发生如下错误:
因此要想解决这种问题就需要更换,不再使用ArrayList
1.3 解决问题
1.Vector
从Vector的源码知道,基本上每个方法都是加锁的,那是不是意味着只要把ArrayList换成Vector就可以解决问题了
修改代码如下:
import java.util.List;
import java.util.Vector;
public class Test {
public static void main(String[] args) {
List<String> list = new Vector<>();
for(int i = 1; i <= 40; i++) {
new Thread(() -> {
list.add(String.valueOf(Math.random()).substring(0,4));
System.out.println(Thread.currentThread().getName() + ":" + list);
},String.valueOf(i)).start();
}
}
}
结果如下:
咋一看,确实能够解决问题。但是从源码可知Vector
是JDK1.0出现,而ArrayList
是从JDK1.2出现。因此Vector
虽然能够解决问题,但是效率却大大的降低了。
2.Collections
当然也可以利用
Collections
的synchronizedList
将线程不安全的ArrayList转换成线程安全的ArrayList
代码如下:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Vector;
public class Test {
public static void main(String[] args) {
List<String> list = Collections.synchronizedList(new ArrayList<>());
for(int i = 1; i <= 40; i++) {
new Thread(() -> {
list.add(String.valueOf(Math.random()).substring(0,4));
System.out.println(Thread.currentThread().getName() + ":" + list);
},String.valueOf(i)).start();
}
}
}
执行结果如下:
从上述结果来看,完全可以解决线程安全问题。
其实从源码来说,就是将每个操作方法加锁。如下:
从上图源码可知,SynchronizedList
继承了SynchronizedCollection
,而通过SynchronizedCollection
的源码可知基本上每个操作方法都加锁了。如下:
这样就可以解决线程安全问题。
3.CopyOnWriteArrayList
3.1简介
该容器是JDK 1.5 提供的一个写时复制容器,位于
java.util.concurrent
,其原理简单理解就是:当并发的往容器写入数据时,不是直接在容器中添加数据,而是将原来的容器复制一份,然后把数据添加到复制的容器中,最后让原来的容器指向复制的容器
当并发读的时候,不需要加锁,直接读取即可
其实我们可以从其源码来了解其原理
从上图源码可知,当创建该容器对象时,就相当于给array
数组赋值
添加数据源码如下:
从上述源码可知,当添加数据时,先开启锁,将array
数组复制到newElements
数组中,并且最后让array
数组指向newElements
数组,最后释放锁。
读取数据源码如下:
从上图源码可知,获取数据时并没有加任何锁。
3.2 操作
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class Test {
public static void main(String[] args) {
List<String> list = new CopyOnWriteArrayList<>();
for(int i = 1; i <= 40; i++) {
new Thread(() -> {
list.add(String.valueOf(Math.random()).substring(0,4));
System.out.println(Thread.currentThread().getName() + ":" + list);
},String.valueOf(i)).start();
}
}
}
结果如下:
3.3 缺点
该容器虽然能够解决并发添加数据问题,但是也存在一定的缺点。
缺点1
当线程读该容器的数据时,而其他的线程正在往容器中写数据,这个时候读取的数据还是原来的旧数据。因为当读取那一刻读取的就是原先旧的数据,新的数据array
还没有指向新的数组
也就是说该容器只能保证数据最终是一致的,而不能保证数据实时一致,如果用户希望数据写操作是实时的,不推荐使用该容器
缺点2
当写入数据时,内从中就会存在两个数组对象:旧数组对象和新数组对象。
假如说旧数组对象中存储的都是大对象,那么插入新的数据后,旧数组对象就会被垃圾回收器回收掉,那么就会造成频繁的垃圾回收。