异常简介
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.ConcurrentModificationException
at 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
@Synchronized
fun 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:只有枚举能防止反射和反序列化调用