JUC:非线程安全的集合类
故障现象
ArrayList、HashMap、HashSet集合都不是线程安全的集合。在简单的单线编程过程中并不会发现这些集合类会出现什么线程相关的错误,而在多线程并发编程过程中如果多个线程并发的操作这些集合可能会报这样的错:
java.util.ConcurrentModificationException
表示并发修改异常
如:
List<String> list = new ArrayList<>();
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(Thread.currentThread().getName() + "\t" + list);
}, String.valueOf(i)).start();
}
导致原因
我们就以ArrayList为例,看下面ArrayList类里面的add方法
如果我们有多个线程想要并发的往ArrayList列表进行插入数据,即调用其add方法,那么每加入一条数据列表长度size就会加1,并且add方法没有加锁,size也没有被volatile修饰,那么我们知道size变化对于多线来说是不可见的,那么就会导致size少加的情况,但是赋值操作一样在执行;这样就有可能会出现数组越界,以及一个线程加的值会覆盖另一个线程之前所加的值,而自己改加的位置没有加即为null。
解决方案
Vector
如下,该方法对add方法进行了加锁synchronized,保证了数据的一致性;
案例如下:
Vector<String> list = new Vector<>();
for (int i = 1; i <= 30; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(Thread.currentThread().getName() + "\t" + list);
}, String.valueOf(i)).start();
}
输出没有问题。
但是,我们并不推荐使用Vector集合类,ArrayList其实是在Vector之后出来的,这说明Vector有 不可取之处,我们都知道synchronized锁效率是很低的,更何况,如果是用在缓存里面,完全是不可取的。
Collections
利用Collections工具类,他为List,Set,Map创建线程安全的示例方法,如下:
List<String> list = Collections.synchronizedList(new ArrayList<String>());
Map<String,Object> map = Collections.synchronizedMap(new HashMap<String,Object>());
Set<Object> set = Collections.synchronizedSet(new HashSet<Object>());
CopyOnWrite容器
CopyonWrite容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器object[]添加,而是先将当前容器object[ ]进行copy,
复制出一个新的容器object[] newElements, 然后新的容器object[] newElements 里添加元素,添加完元素之后,再将原容器的引用指向新的容器setArray(newElements);。这样做的好处是可以对Copyonwrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以Copyonwrite 容器也是一种读 写分离的思想,读和写不同的容器。
读的是共享的容器,写的是copy过来的容器。当线程要执行add操作时,所有线程同一时刻只能有一个线程可以进行写操作,当这个线程写完再将原容器指向新的容器后,其他线程才能进入add操作。
其底层原理其实就是利用的volatile修饰的数组作为容器,其add方法加了lock锁。
那么其对应的优化如下:
Set<String> set = new CopyOnWriteArraySet<>(); //其底层new的还是CopyOnWriteArrayList
List<String> list = new CopyOnWriteArrayList<String>();
Map<String,Object> map = new ConcurrentHashMap<>();