异常简介
ConcurrentModificationException(并发修改异常)是基于java集合中的 快速失败(fail-fast) 机制产生的,在使用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了增删改,就会抛出该异常。
快速失败机制使得java的集合类不能在多线程下并发修改,也不能在迭代过程中被修改。
异常原因
示例代码
val elements : MutableList<Int> = mutableListOf()
for ( i in 0..100) {//添加元素elements.add(i)
}val thread = Thread {//线程一读数据elements.forEach {Log.i("testTag", it.toString())}
}val thread2 = Thread {//线程二写入数据for (i in 1..100) {elements.add(i)}
}thread.start()
thread2.start()抛出异常:java.util.ConcurrentModificationExceptionat java.util.ArrayList$Itr.next(ArrayList.java:860)
异常原因是什么呢?
modCount:表示list集合结构上被修改的次数
expectedModCount:表示对ArrayList修改次数的期望值(在开始遍历元素之前记录的)
list的for循环中是通过Iterator迭代器遍历访问集合内容,在遍历过程中会使用到modCount变量,如果在遍历过程期间集合内容发生变化,则会改变modCount的数值,每当迭代器使用next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedModCount 值,相等的话就返回遍历;否则抛出异常(ConcurrentModificationException),终止遍历。
而在我们的示例代码中,线程二在调用add方法的时候modCount+1,导致线程一在遍历的时候modCount!=expectedModCount,所以抛出了ConcurrentModificationException
解决方法
那在多线程下,我们需要集合支持并发读写怎么实现呢?
- 使用Collections.synchronizedList给集合加锁
val elements : MutableList<Int> = Collections.synchronizedList(mutableListOf())
...
val thread = Thread {//线程一读数据synchronized(elements) {//使用Iterator遍历时需要手动加锁elements.forEach {Log.i("testTag", it.toString())}}
}
...
原理:
以组合的方式将对 List 的接口方法操作,委托给传入的 list 对象,并且对所有的接口方法对象加锁,得到并发安全性。通过组合的方式对传入的list对象的get,set,add等方法加synchronized同步锁,但是对于需要用到iterator迭代器的时候需要手动加锁
public static <T> List<T> synchronizedList(List<T> list) {return (list instanceof RandomAccess ?new SynchronizedRandomAccessList<>(list) :new SynchronizedList<>(list));
}static <T> List<T> synchronizedList(List<T> list, Object mutex) {return (list instanceof RandomAccess ?new SynchronizedRandomAccessList<>(list, mutex) :new SynchronizedList<>(list, mutex));
}SynchronizedCollection(Collection<E> c) {this.c = Objects.requireNonNull(c);//需要加锁的对象,这里指自己mutex = this;
}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;}SynchronizedList(List<E> list, Object mutex) {super(list, mutex);this.list = list;}//在list提供的方法外加了synchronized同步锁public boolean equals(Object o) {if (this == o)return true;synchronized (mutex) {return list.equals(o);}}public int hashCode() {synchronized (mutex) {return list.hashCode();}}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);}}public int indexOf(Object o) {synchronized (mutex) {return list.indexOf(o);}}public int lastIndexOf(Object o) {synchronized (mutex) {return list.lastIndexOf(o);}}public boolean addAll(int index, Collection<? extends E> c) {synchronized (mutex) {return list.addAll(index, c);}}//使用iterator迭代器的时候需要手动加锁public ListIterator<E> listIterator() {return list.listIterator(); // Must be manually synched by user}public ListIterator<E> listIterator(int index) {return list.listIterator(index); // Must be manually synched by user}
优点:可以使非线程安全的集合如Arraylist封装成线程安全的集合,并且相对CopyOnWriteArrayList写操作性能较好
缺点:在任何操作之前都需要加同步锁,使用iterator还需要手动加锁才能保证并发读写安全
2. 使用支持并发读写的CopyOnWriteArrayList
val elements : CopyOnWriteArrayList<Int> = CopyOnWriteArrayList()
原理:
public E get(int index) {return get(getArray(), index);
}public boolean add(E e) {synchronized (lock) {Object[] elements = getArray();int len = elements.length;Object[] newElements = Arrays.copyOf(elements, len + 1);newElements[len] = e;setArray(newElements);return true;}
}
...
读操作:直接读数组对应位置的数据
写操作:以add方法为例,在执行add方法时,会先对集合对象添加同步锁,然后创建一个len+1的数组,再把旧数组中数据复制添加到新数组中,最后把新数组替换掉老数组
优点:读操作效率高,无加锁操作
缺点:写操作每次都需要复制一份新数组,性能较差
拓展:多线程下怎么做好单例的设计
懒汉式单例
在需要的时候再去创建实例。
锁它!锁它!锁它!
同步锁
Java
public class SingleTon {private static volatile SingleTon instance;private SingleTon() {}public static SingleTon getInstance() {synchronized (SingleTon.class) {if (instance == null) {instance = new SingleTon();}} return instance;}
}
Kotlin
class SingleTon {companion object {private var instance: SingleTon? = null@Synchronizedfun getInstance(): SingleTon {if (instance == null) {instance = SingleTon()}return instance!!}}
}
优点:线程安全,可以延时加载。
缺点:调用效率不高(有锁,且需要先创建对象)。
DCL
为提升性能,减小同步锁的开销,避免每次获取实例都需要经过同步锁,可以使用双重检测判断实例是否已经创建。
Java
public class SingleTon {private static volatile SingleTon4 instance;private SingleTon() {}public static SingleTon getInstance() {if (instance == null) {synchronized (SingleTon.class) {if (instance == null) {instance = new SingleTon4、();}}}return instance;}
}
Kotlin
class SingleTon4 {companion object {val instance by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {SingleTon4()}}
}
饿汉式单例
在类被加载的时候就把Singleton实例给创建出来供使用,以后不再改变。
Java
public class SingleTon {private static SingleTon singleTon = new SingleTon();private SingleTon() {}public static SingleTon getInstance() {return singleTon;}
}
Kotlin
object SingleTon1 {}
优点:实现简单, 线程安全,调用效率高(无锁,且对象在类加载时就已创建,可直接使用)。
缺点:可能在还不需要此实例的时候就已经把实例创建出来了,不能延时加载(在需要的时候才创建对象)。
静态内部类
静态内部类只有被主动调用的时候,JVM才会去加载这个静态内部类。外部类初次加载,会初始化静态变量、静态代码块、静态方法,但不会加载内部类和静态内部类。
Java
public class Singleton {private Singleton() {}public static Singleton getInstance() {return SingletonFactory.instance;}private static class SingletonFactory {private static Singleton instance = new Singleton();}}
Kotlin
class SingleTon5 {companion object {fun getInstance() = Holder.instance}private object Holder {val instance = SingleTon5()}
}
优点:线程安全,调用效率高,可以延时加载。
枚举类
最佳的单例实现模式就是枚举模式。写法简单,线程安全,调用效率高,可以天然的防止反射和反序列化调用,不能延时加载。
Java
public enum Singleton {INSTANCE;public void show() {System.out.println("show");}}调用Singleton.INSTANCE.show();
Kotlin
enum class Singleton {INSTANCE;fun show() {println("show")}
}
写在最后:
在线程安全的几种单例中
枚举(无锁,调用效率高,可以防止反射和反序列化调用,不能延时加载)> 静态内部类(无锁,调用效率高,可以延时加载) > 双重同步锁(有锁,调用效率高于懒汉式,可以延时加载) > 懒汉式(有锁,调用效率不高,可以延时加载) ≈ 饿汉式(无锁,调用效率高,不能延时加载)
ps:只有枚举能防止反射和反序列化调用