1、出现现象
当我们用多个线程对ArrayList进行添加操作的时候,会出现异常信息 ConcurrentModificationException
下面放上代码
public class ListTest {
public static void main(String[] args) {
//ArrayList 不安全的 synchronized
List<Object> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
2、解决办法
由上方的案例可以发现,当多线程的环境下ArrayList会出现异常,list、map在日常开发学习中都是使用的比较多的,这个问题如何解决呢?
第一种 比较简单的方法,我们可以用 synchronized 修饰,此时无报错信息
List<Object> list = new ArrayList<>();
Object obj = new Object();
for (int i = 0; i < 100; i++) {
new Thread(()->{
synchronized (obj){
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
}
},String.valueOf(i)).start();
}
此外还可以用List list = new Vector<>();来解决,在他的每个add方法里面加了修饰,无异常报错
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
第三种方法 使用Collections.synchronizedList方法解决,此时代码正常运行,
List<Object> list = Collections.synchronizedList(new ArrayList<>());
for (int i = 0; i < 100; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
这又是为何呢?点进去查看源码,原来在SynchronizedList里面对每个方法用synchronized +mutex对象锁的方式进行了保护,阻止了并发修改
//点击synchronizedList,查看源码
List<Object> list = Collections.synchronizedList(new ArrayList<>());
===========================
//Collections里面的synchronizedList方法
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list)); //最终都是要走到这个里面
}
//这里我们传的参数是ArrayList,他是RandomAccess的实现类(ArrayList实现了它),
//走的第一个SynchronizedRandomAccessList
===========================
//再来看Collections的内部类SynchronizedRandomAccessList,它继承了SynchronizedList
static class SynchronizedRandomAccessList<E>
extends SynchronizedList<E> implements RandomAccess {
SynchronizedRandomAccessList(List<E> list) {
super(list);//点进去,走到了SynchronizedList
}
}
===========================
//Collections的内部类SynchronizedList,它继承了SynchronizedCollection
static class SynchronizedList<E>
extends SynchronizedCollection<E> implements List<E> {
private static final long serialVersionUID = -7754090372962971524L;
final List<E> list;
SynchronizedList(List<E> list) {
super(list);//点进去
this.list = list;
}
//可以看到这里的 get/set方法中都看到了synchronized代码快,其中使用的是mutex进行修饰
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
public E remove(int index) {
synchronized (mutex) {return list.remove(index);}
}
}
===========================
//Collections内部类SynchronizedCollection
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
private static final long serialVersionUID = 3053995032091335093L;
final Collection<E> c; // Backing Collection
final Object mutex; // Object on which to synchronize
SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
mutex = this;
}
}
以上三种方法本质上都是加了Synchronized来解决他们的并发问题,下面提供一种新的思路吧。
.
第四种:使用CopyOnWriteArrayList的方式对list进行添加,此时也是正常运行
List<String> list = new CopyOnWriteArrayList<>();
Object obj = new Object();
for (int i = 0; i < 100; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
CopyOnWriteArrayList 英语并不好的我也能通过名字看出,在写的时候复制,简称写时复制
在来看一下CopyOnWriteArrayList 他的源代码
List<String> list = new CopyOnWriteArrayList<>();//点进去
lass CopyOnWriteArrayList<E>{
//volatile 修饰,保证可见性和有序性,不保证原子性
private transient volatile Object[] array;
//构造函数创建了一个长度为0的Object数组
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); //加锁
try {
Object[] elements = getArray(); //取得老数组
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);//将原有数组元素值赋值给新数组
newElements[len] = e;//添加
setArray(newElements);//设置新的值
return true;
} finally {
lock.unlock();//解锁
}
}
}
从上面的代码可以看到,我们在做add操作的时候,先创建了一个新的数组,然后将原有数组数据复制到新数组种,最好在新数组末尾添加value,这些过程通过加锁修饰了,解决了他们的并发问题。
CopyOnWriteArrayList为什么并发安全且性能比Vector好
Vector是增删改查方法都加了synchronized,保证同步,但是每个方法执行的时候都要去获得
锁,性能就会大大下降,而CopyOnWriteArrayList 只是在增删改上加锁,但是读不加锁,在读方面的性
能就好于Vector,CopyOnWriteArrayList支持读多写少的并发情况。
不过这也要根据具体实际情况来考虑,上面代码也能看到,每次添加的时候都会创建一个数组对象,新的数组对象长度只是老数组长度+1,这在写操作多的时候 开销也是很大的,需要不断地区创建对象,复制数据。中间过程中也浪费了许多的内存空间,加大了垃圾回收的程度。
public class ListTest {
public static void main(String[] args) {
//ArrayList 不安全的 synchronized
/*解决方法
List<Object> list = new Vector<>();
List<Object> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
* */
//CopyOnWrite 写入时复制 cow 优化策略
//多个线程调用时,list 读取时时固定的,写入(覆盖)
//在写入时避免覆盖 造成数据问题
List<String> list = new CopyOnWriteArrayList<>();
// List<Object> list = new ArrayList<>();
Object obj = new Object();
for (int i = 0; i < 100; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
Set解决办法总体相似
也是上面几个方法,过程就不叙述了,上码吧
/**
* java.util.ConcurrentModificationException
*/
public class SetTest {
public static void main(String[] args) {
// Set<String> set = new HashSet<>();
/*解决方法
Set<String> set = new HashSet<>();
Set<String> set = Collections.synchronizedList(new HashSet<>());
Set<String> set = new CopyOnWriteArraySet<>();
* */
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 40; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
3、Map
Map的话 可以使用Hashtable 、 Collections.synchronizedMap、ConcurrentHashMap三种方式
public class MapTest {
public static void main(String[] args) {
//new HashMap(16,0.75)
// Map<String, String> map = new HashMap<>();
// Map<Object, Object> map = new Hashtable<>();
// Map<Object, Object> map = Collections.synchronizedMap(new HashMap<>());
Map<String, String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 40; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(),UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
前面两种比较简单,第三种看链接https://blog.csdn.net/weixin_44460333/article/details/86770169