ConcurrentModificationException异常原因,解决方法,线程安全的单例模式

异常简介

ConcurrentModificationException(并发修改异常)是基于java集合中的 快速失败(fail-fast) 机制产生的,在使用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了增删改,就会抛出该异常。
快速失败机制使得java的集合类不能在多线程下并发修改,也不能在迭代过程中被修改。

异常原因

示例代码

val elements : MutableList = 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),终止遍历。
ConcurrentModificationException异常原因,解决方法,线程安全的单例模式_第1张图片

而在我们的示例代码中,线程二在调用add方法的时候modCount+1,导致线程一在遍历的时候modCount!=expectedModCount,所以抛出了ConcurrentModificationException

解决方法

那在多线程下,我们需要集合支持并发读写怎么实现呢?

  1. 使用Collections.synchronizedList给集合加锁
val elements : MutableList = 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  List synchronizedList(List list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}

static  List synchronizedList(List list, Object mutex) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list, mutex) :
            new SynchronizedList<>(list, mutex));
}

SynchronizedCollection(Collection c) {
    this.c = Objects.requireNonNull(c);
    //需要加锁的对象,这里指自己
    mutex = this;
}

static class SynchronizedList
    extends SynchronizedCollection
    implements List {
    private static final long serialVersionUID = -7754090372962971524L;


    final List list;

    SynchronizedList(List list) {
        super(list);
        this.list = list;
    }
    SynchronizedList(List 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 c) {
        synchronized (mutex) {return list.addAll(index, c);}
    }
    
    //使用iterator迭代器的时候需要手动加锁
    public ListIterator listIterator() {
        return list.listIterator(); // Must be manually synched by user
    }

    public ListIterator listIterator(int index) {
        return list.listIterator(index); // Must be manually synched by user
    }

优点:可以使非线程安全的集合如Arraylist封装成线程安全的集合,并且相对CopyOnWriteArrayList写操作性能较好
缺点:在任何操作之前都需要加同步锁,使用iterator还需要手动加锁才能保证并发读写安全
2. 使用支持并发读写的CopyOnWriteArrayList

val elements : CopyOnWriteArrayList = 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:只有枚举能防止反射和反序列化调用

你可能感兴趣的:(Android,开发之路,单例模式,java,jvm)