场景
因为在工作的时候,不会去过度的考虑高并发的问题。大多数集合的使用都是一般的list,set,map。也不会有太大的问题。但是如果放在多线程的场景下,多个线程同时去操纵一个集合,问题甚多。
集合安全问题代码展示
List
这里是一段代码
List<String> list = new ArrayList<>();
for (int i = 0; i < 300; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(10));
System.out.println(list);
}).start();
}
这是部分执行结果
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at java.util.AbstractCollection.toString(AbstractCollection.java:461)
at java.lang.String.valueOf(String.java:2994)
at java.io.PrintStream.println(PrintStream.java:821)
at com.anxin.juc.listThread.ListThread.lambda$main$0(ListThread.java:20)
at java.lang.Thread.run(Thread.java:748)
java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at java.util.AbstractCollection.toString(AbstractCollection.java:461)
at java.lang.String.valueOf(String.java:2994)
at java.io.PrintStream.println(PrintStream.java:821)
at com.anxin.juc.listThread.ListThread.lambda$main$0(ListThread.java:20)
at java.lang.Thread.run(Thread.java:748)
java.util.ConcurrentModificationException
对于ConcurrentModificationException异常,意思是并发修改异常,我们在打印这个集合的时候就会爆出这个异常,包括有时候我们一边遍历一边删除的时候也会出现这个异常,这个就要用迭代器解决了。扯远了。
那么在JUC中如何解决这个问题。解决方案无外乎有三种。
1.用Vector集合
List<String> list = new Vector<>();
for (int i = 0; i < 300; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(10));
System.out.println(list);
}).start();
}
这个Vector是线程安全的单线程集合。可以解决这个问题。他的方法几乎都用同步锁修饰。但是自我从业以来,没用过这个,这个也是被废弃的,因为这个性能太低了。而且同步锁并不能真正的用于一些并发量极大的场景。
2.用 Collections.synchronizedList
List<String> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 300; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(10));
System.out.println(list);
}).start();
}
源码是这样的:
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<T>(list) :
new SynchronizedList<T>(list));
}
不难解释,为什么他是线程安全的。
3.用 CopyOnWriteArrayList
List<String> list =new CopyOnWriteArrayList();
for (int i = 0; i < 300; i++) {
new Thread(() -> {
list.add(UUID.randomUUID().toString().substring(10));
System.out.println(list);
}).start();
}
这个主要是写时复制技术实现的。
我们看一个代码,猜猜它会不会报错?
List<String> list =new CopyOnWriteArrayList();
list.add("111");
list.add("222");
list.add("333");
list.add("444");
list.add("555");
for (String s : list) {
list.remove(s);
System.out.println(list);
}
不会报错!为什么?
因为
当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行 Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。
这时候会抛出来一个新的问题,也就是数据不一致的问题。如果写线程还没来得及写会内存,其他的线程就会读到了脏数据。==这就是 CopyOnWriteArrayList 的思想和原理。就是拷贝一份。
- 它最适合于具有以下特征的应用程序:List 大小通常保持很小,只读操作远多
于可变操作,需要在遍历期间防止线程间的冲突。 - 它是线程安全的。
- 因为通常需要复制整个基础数组,所以可变操作(add()、set() 和 remove()
等等)的开销很大。 - 迭代器支持 hasNext(), next()等不可变操作,但不支持可变 remove()等操作。
- 使用迭代器进行遍历的速度很快,并且不会与其他线程发生冲突。在构造迭代
器时,迭代器依赖于不变的数组快照。 - 独占锁效率低:采用读写分离思想解决
- 写线程获取到锁,其他写线程阻塞
- 复制思想
Set
Set set = new CopyOnWriteArraySet<>();
解决问题
Map
HashTable
Map<String,String> map = new Hashtable<>();
for (int i = 0; i < 300; i++) {
new Thread(() -> {
map.put(UUID.randomUUID().toString().substring(8),UUID.randomUUID().toString().substring(16));
System.out.println(map);
}).start();
}
有必要说一下hashtable,底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率不是很高,不推荐使用。
ConcurrentHashMap
Map<String,String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 300; i++) {
new Thread(() -> {
map.put(UUID.randomUUID().toString().substring(8),UUID.randomUUID().toString().substring(16));
System.out.println(map);
}).start();
}
推荐使用这个,那为什么推荐?必须先明白一个问题,为什么该集合线程安全?
ConcurrentHashMap是由一个Segment数组和多个HashEntry数组组成
Segment是一种可重入锁ReentrantLock),ConcurrentHashMap里面扮演锁的角色;HashEntry则用于存储键值对数据。
那么为什么他的效率高呢?
这就是我之前说为为何同步锁根本扛不住很大并发的原因。因为HashTable之流,是很多线程去争夺一把锁。而ConcurrentHashMap的锁的粒度很小,每个分段去分一把锁。所以效率高。
总结
1.线程安全与线程不安全集合
集合类型中存在线程安全与线程不安全的两种,常见例如:
ArrayList ----- Vector
HashMap -----HashTable
但是以上都是通过 synchronized 关键字实现,效率较低
2.Collections 构建的线程安全集合
3.java.util.concurrent 并发包下
CopyOnWriteArrayList CopyOnWriteArraySet 类型,通过动态数组与线程安全个方面保证线程安全。
预告
下一节是volatile关键字。