04_并发容器类
1. 重现线程不安全:List
首先以List作为演示对象,创建多个线程对List接口的常用实现类ArrayList进行add操作。
public class NotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
测试结果:
出现了线程不安全错误
ArrayList在多个线程同时对其进行修改的时候,就会抛出java.util.ConcurrentModificationException异常(并发修改异常),因为ArrayList的add及其他方法都是线程不安全的,有源码佐证:
解决方案:
List接口有很多实现类,除了常用的ArrayList之外,还有Vector和SynchronizedList。
他们都有synchronized关键字,说明都是线程安全的。
改用Vector或者synchronizedList试试:即可解决!
public static void main(String[] args) {
//List<String> list = new Vector<>();
List<String> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 200; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
Vector和Synchronized的缺点:
vector:内存消耗比较大,适合一次增量比较大的情况
SynchronizedList:迭代器涉及的代码没有加上线程同步代码
2. CopyOnWrite容器
什么是CopyOnWrite容器?
CopyOnWrite容器(简称COW容器)即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。
先看看CopyOnWriteArrayList类:发现它的本质就是数组
再来看看CopyOnWriteArrayList的add方法:发现该方法是线程安全的
使用CopyOnWriteArrayList改造main方法:
public static void main(String[] args) {
//List<String> list = new Vector<>();
//List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 200; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
CopyOnWrite并发容器用于读多写少的并发场景。比如:白名单,黑名单。假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单一定周期才会更新一次。
缺点:
内存占用问题。写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存。通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。
3. 扩展类比:Set和Map
HashSet和HashMap也都是线程不安全的,类似于ArrayList,也可以通过代码证明。
private static void notSafeMap() {
Map<String, String> map = new HashMap<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
map.put(String.valueOf(Thread.currentThread().getName()), UUID.randomUUID().toString().substring(0, 8));
System.out.println(map);
}, String.valueOf(i)).start();
}
}
private static void notSafeSet() {
Set<String> set = new HashSet<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(set);
}, String.valueOf(i)).start();
}
}
都会报:ConcurrentModificationException异常信息。
Collections提供了方法synchronizedList保证list是同步线程安全的,Set和Map呢?
HashMap<String, String> map = new HashMap<>(); //不安全
Hashtable<String, String> map = new Hashtable<>(); //安全
Map<String, String> map = Collections.synchronizedMap(new HashMap<>()); //安全
ConcurrentMap<String, String> map = new ConcurrentMap<>(); //安全
JUC提供的CopyOnWrite容器实现类有:CopyOnWriteArrayList和CopyOnWriteArraySet。
有没有Map的实现:
最终实现:
public class NotSafeDemo {
public static void main(String[] args) {
notSafeList();
notSafeSet();
notSafeMap();
}
private static void notSafeMap() {
//Map<String, String> map = new HashMap<>();
//Map<String, String> map = Collections.synchronizedMap(new HashMap<>());
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
map.put(String.valueOf(Thread.currentThread().getName()), UUID.randomUUID().toString().substring(0, 8));
System.out.println(map);
}, String.valueOf(i)).start();
}
}
private static void notSafeSet() {
//Set<String> set = new HashSet<>();
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 30; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(set);
}, String.valueOf(i)).start();
}
}
private static void notSafeList() {
//List<String> list = new Vector<>();
//List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0, 8));
System.out.println(list);
}, String.valueOf(i)).start();
}
}
}
扩展:HashSet底层数据结构是什么?HashMap ?
但HashSet的add是放一个值,而HashMap是放K、V键值对
4. 并发容器和同步容器
同步容器可以简单地理解为通过synchronized来实现同步的容器。同步容器会导致多个线程中对容器方法调用的串行执行,降低并发性,因为它们都是以容器自身对象为锁。在并发下进行迭代的读和写时并不是线程安全的。如:Vector、Stack、HashTable、Collections类的静态工厂方法创建的类(如Collections.synchronizedList)
并发容器是针对多个线程并发访问而设计的,在jdk5.0引入了concurrent包,其中提供了很多并发容器,如ConcurrentHashMap、CopyOnWriteArrayList等。
ConcurrentHashMap:内部采用Segment结构,进行两次Hash进行定位,写时只对Segment加锁
CopyOnWriteArrayList:CopyOnWrite写时复制一份新的,在新的上面修改,然后把引用指向新的。只能实现数据的最终一致性,非实时一致的;代替List,适用于读操作为主的情况
同步容器与并发容器都为多线程并发访问提供了合适的线程安全,不过并发容器的可扩展性更高。
public static void main(String[]args){
Vector v = new Vector();
for (int i = 0; i < 10; i++) {
int a = i;
new Thread(()->{
v.add(a);
}).start();
}
for (Iterator iterator = v.iterator(); iterator.hasNext();) {
int element = (int) iterator.next();
v.remove(element);
}
}
5. 性能测试(了解)
① 引入spring-core依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.18</version>
</dependency>
② 测试代码
StopWatch stopwatch = new StopWatch();
public static void main(String[] args) {
StopWatch stopwatch = new StopWatch();
List<Integer> list = new Vector<>();
stopwatch.start("Vector:write数据");
IntStream.rangeClosed(1,1000000).parallel().forEach( a ->{
list.add(new Random().nextInt(1000000));
});
stopwatch.stop();
stopwatch.start("Vector:read数据");
IntStream.rangeClosed(1,1000000).parallel().forEach( a ->{
list.get(new Random().nextInt(1000000));
});
stopwatch.stop();
System.out.println(stopwatch.prettyPrint());
}
③ 结果