一 、集合的线程不安全举例
ArrayList中的add方法并没有synchronized关键字,是线程不安全的,源码如下:
演示:
package com.jess.sync;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @program: JUC
* @description: list集合线程不安全
* @author: Jess
* @create: 2021-07-23 14:36
**/
public class ThreadDemo4 {
public static void main(String[] args) {
//创建ArrayList集合
List<String> list = new ArrayList<>();
for (int i = 0; i <30; i++) {
new Thread(()->{
//向集合添加内容
list.add(UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
代码执行报错:
ConcurrentModificationException:并发修改异常
二、ArrayList解决方案
1. Vector解决
该方案不常用,是很久远的方案
public class ThreadDemo4 {
public static void main(String[] args) {
//Vector解决
List<String> list = new Vector<>();
for (int i = 0; i <30; i++) {
new Thread(()->{
//向集合添加内容
list.add(UUID.randomUUID().toString().substring(0,8));
//从集合获取内容
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
Vector的扩容机制和ArrayList很像。
如果指定了扩容容量的大小则扩容的新数组大小为原来的数组加上扩容容量的大小,如果不指定扩容容量的大小则扩容的新数组大小为原来数组大小的2倍。
2. Collections解决
不常用
//Collections解决
//synchronizedList:返回指定列表支持的同步(线程安全的)列表
List<String> list = Collections.synchronizedList(new ArrayList<>());
3*. CopyOnWriteArrayList解决
常用解决方案----写时复制技术:在写操作时先复制一份和之前的集合相同大小的内容,复制之后向新集合里面写入新的内容,写完后再将之前的集合覆盖,这样就可以保证写操作不会影响读操作了。
CopyOnWriteArrayList 类的所有可变操作(add,set等等)都是通过创建底层数组的新副本来实现的。
List<String> list = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList写入操作add()方法在添加集合的时候加了Lock锁,保证同步,避免多线程写的时候会复制出多个副本。
读取操作没有任何同步控制和锁操作,理由就是内部数组array不会发生修改,只会被另外一个array替换,因此可以保证数据安全。
为什么使用CopyOnWriteArrayList而不用Vector呢?
- Vector集合的所有操作元素的方法都加了synchronized关键字,这就导致了操作Vector的效率会非常低。而CopyOnWriteArrayList的读操作是不加锁的,因此CopyOnWriteArrayList的读性能远高于Vector。
- Vector每次扩容的大小都是原来数组大小的2倍,而CopyOnWriteArrayList不需要扩容,通过COW思想就能使数组容量满足要求。
三、HashSet解决方案
public class ThreadDemo4 {
public static void main(String[] args) {
// 演示Hashset
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();
}
}
}
与ArrayList一样有线程安全问题:
add方法源码:
解决方法同ArrayList:
//工具类解决方法
Set<String> set1 = Collections.synchronizedSet(new HashSet<>());
//juc解决方法 cow
Set<String> set = new CopyOnWriteArraySet<>();
HashSet底层是什么?就是HashMap
Set的add()方法本质就是利用PRESENT隔离键值对中的值,只put map的key,又因为key是不能重复的,所以set也是无重复元素的的。
四、HashMap解决方案
HashMap不安全,通过ConcurrentHashMap来解决。
Map<String,String> map = new ConcurrentHashMap<>();
虽然在并发场景下 HasTable 和由同步包装器包装的HashMap(Collections.synchronizedMap(Map<K,V> m) )都可以代替HashMap,但是它们都是通过使用一个全局的锁来同步不同线程间的并发访问,因此会带来不可忽视的性能问题。
ConcurrentHashMap是一个支持高并发更新与查询的哈希表,它是基于HashMap的。
- 在ConcurrentHashMap中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问。
- 它由多个 Segment(段) 组合而成。Segment 本身就相当于一个 HashMap 对象。同 HashMap 一样,Segment 包含一个 HashEntry 数组,数组中的每一个 HashEntry 既是一个键值对,也是一个链表的头节点。