解决集合并发不安全问题
1. List并发不安全
1、在并发环境下往list集合中添加或修改数据会出现ConcurrentModificationException异常(并发修改异常)
-
方法一:使用vector集合
List<String> list = new Vector<>();
-
方法二:使用collections工具类
List<E> list = Collections.synchronizedList(new ArrayList<>());
-
使用java.util.concurrent下的CopyOnWriteArrayList集合
- 其底层也是一个数组实现。private transient volatile Object[] array; 由volatile修饰;
- 它是一个线程安全的ArrayList;
- 使用独占锁ReentrantLock来保证同时只有一个线程对array进行修改:final transient ReentrantLock lock = new ReentrantLock();
//创建一个并发安全的list集合 List<String> list = new CopyOnWriteArrayList<>(); public CopyOnWriteArrayList() { setArray(new Object[0]); } final void setArray(Object[] a) { array = a; //array是不可序列化且由volatile修饰的 } private transient volatile Object[] array; //底层源码
2、CopyOnWrite :写入时复制 。简称COW 是一种优化策略。
- 多线程使用list,写入时可能会存在覆盖操作。先复制一份,再给调用者。
- 读写分离
3、如果让我们自己做一个写时复制的线程安全的list应该怎么做???需要考虑以下几点:
- 何时初始化list,初始化的list元素个数为多少,list是有限大小吗??
- 如何保证线程安全,比如多个线程进行读写时如何保证是线程安全的??
- 如何保证使用迭代器遍历list时的数据一致性??
解决上述三个问题:
//无参构造:创建一个大小为 0 的数组作为array的初始值
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
//两个有参构造初始化数组
//1、创建一个list,其内部元素是入参toCopyIn的副本
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
//入参为一个集合,将集合里面的元素复制到 list集合中
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
添加元素的实现:
public boolean add(E e) {
final ReentrantLock lock = this.lock; //获取独占锁,如果多个线程都调用add方法则只有一个线程会获取到该锁,其他线程会被阻塞挂起直到锁被释放。保证线程安全
lock.lock();
try {
Object[] elements = getArray(); //获取array
//复制array到一个新数组(新数组是原数组的大小加 1, 所以CopyOnWriteArrayList是一个无界数组),并把新增的元素添加到新数组。
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
//使用新数组替换添加前的数组
setArray(newElements);
return true;
} finally {
//在返回前释放锁,先执行finally代码块,再执行return语句
lock.unlock();
}
}
总结:
- CopyOnWriteArrayList使用写时复制的策略来保证List的一致性。
- 获取-修改-写入三步操作不是原子性的,所以在增删改的过程中都使用了独占锁,来保证在某个时间只有一个线程能对 list的数组进行修改
- CopyOnWriteArrayList 提供了弱一致性的迭代器,即保证在获取迭代器后,其他线程对list的修改是不可见的(即修改是获取下一个迭代器时才生效)
2. Set并发不安全
方法一:使用collections工具类
Set<String> set= Collections.synchronizedSet(new HashSet<>());
方法二:使用java.ytil.concurrent下的CopyOnWriteArraySet集合。
Set<String> set= new CopyOnWriteArraySet<>();
HashSet的底层就是一个HashMap。
public HashSet() {
map = new HashMap<>();
}
// HashSet的add()方法的底层
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
private static final Object PRESENT = new Object(); //不变的值
3. Map并发不安全
Map<String, String> map = new HashMap<>();
-
map是这样用的吗? 不是。
-
默认等价于什么? Map<String, String> map = new HashMap<>(16, 0.75);
-
加载因子(0.75),初始化容量(16)
- 底层是写成位运算:1 < < 4 (初始化容量);
- 加载因子:static final float DEFAULT_LOAD_FACTOR = 0.75f;
- 最大容量: 1 << 30;
-
解决:使用JUC包下的ConcurrentHashMap集合
import java.util.concurrent.ConcurrentHashMap; Map<String, String> map =new ConcurrentHashMap<>();