以前在Java基础学习集合中,ArrayList听说线程不安全,但现在还不知道为什么不安全。
import java.util.ArrayList; import java.util.List; import java.util.UUID; publicclasscopyonwrite _ list { publicstaticvoidmain (string [ ] args (//资源类ListObject list=new ArrayList ) ); //10创建线程并将元素for添加到资源类对象中(intI=1; i=10; I ) ) newthread((-) list.add ) uuid.Randomuuid ).toString ).substring ),0,5 ); system.out.println(list; },string.valueof(I ) ).start ); }}运行结果
执行程序时发生错误,抛出concurrentmodificationexception异常。 此例外称为并发修正例外,让我们分析一下问题的原因
访问ArrayList的源代码以验证add方法。
publicbooleanadd(ee ) ensurecapacityinternal (size1); //Increments modCount! elementData[size ]=e; 返回真; }这里调用了ensureCapacityInternal方法。 进入这个方法。
privatevoidensurecapacityinternal (int min capacity ) if ) element data==default capacity _ empty _ element data ) mincapapacitity } ensureCapacityInternal ()此方法的作用是,如果将当前新元素添加到列表后面,则确定是否满足列表的elementData数组的大小。 如果size 1的此需求长度大于elementData数组的长度,则必须扩展该数组
由此可见,当看到add元素时,实际上采取了两大步骤:
由于没有设置同步操作来为确定elementData数组的容量是否符合要求的elementData的相应位置设置值,因此首先会危及线程的安全性,并且在多个线程上执行add操作可能会导致elementData数组越界。
例如:
列表大小为9,即size=9
线程a开始进入add方法,此时获取的size的值为9,调用ensureCapacityInternal方法进行容量判断。 线程b此时也进入add方法,获取的size的值也为9,ensureCapacityInternal方法的调用也开始。 线程a发现需求大小为10,元素数据大小为10,可以容纳。 然后,它没有扩张,而是回来了。 线程b也发现需求大小为10,可以容纳,然后返回。 线程a开始执行设置值操作,并开始执行elementData[size ]=e操作。 此时size为10。 线程b也开始了设置值操作,并尝试设置elementData[10]=e。 另一方面,elementData没有扩展,下标最多为9。 因此,报告了序列越界的异常ArrayIndexOutOfBoundsException。 此外,在步骤2中将elementData[size ]=e设置为值的操作也损害了线程的安全性。 由此可知,该步骤操作也不是原子操作,而是由以下两个步骤操作构成。
elementData[size]=e; size=size 1; 虽然这两个代码在单线程中运行时没有问题,但在多线程环境中运行时,一个线程的值可能会复盖另一个线程添加的值。 具体逻辑如下。
如果:
列表大小为0,即size=0
线程a开始添加元素,值为a。 此时,执行了第一个操作,将a放在了elementData下标为0的位置。 线程b也正好开始添加具有b值的元素,然后进入第一步。 此时,由于线程b获取的size的值依然为0,因此b也置于elementData下标为0的位置。 线程a开始将size的值增加为1,线程b开始将size的值增加为2,线程AB完成执行后,理想情况下size为2,elementData下标0的位置为a,下标1的位置为b。 实际情况是size为2,elementData下标为0的位置为b,下标为1的位置处没有任何东西。 此后,除非使用set方法更改此位置的值,否则始终为null。 这是
为size为2,添加元素时会从下标为2的位置上开始。由此可以看出,ArrayList是线程不安全的,对此有两种解决方法:
使用vector代替ArrayList(不推荐)使用Collections提供的方法synchronizedList来保证list是同步线程安全(不建议)使用Vector:
import java.util.List;import java.util.UUID;import java.util.Vector;public class CopyOnWrite_List { public static void main(String[] args) { List<Object> list = new Vector<>(); for (int i = 1; i <= 10; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(list); },String.valueOf(i)).start(); } }}运行结果:
可以看到,使用了vector后,程序并没有抛出异常,但是为什么不推荐使用呢?我们查看一下vector对add操作的源码:
public synchronized boolean add(E e) { modCount++; ensureCapacityHelper(elementCount + 1); elementData[elementCount++] = e; return true;}显然,由于在方法上使用了同步操作,使得多个线程在操作这个集合时是同步执行的,因此在进行添加时,不会产生并发问题。而vector在jdk1.0就存在了,而ArrayList在Vector之后才出现,当初 JDK1.0 在开发时,可能为了快速的推出一些基本的数据结构操作,所以推出了一些比较粗糙的类。比如,Vector、Stack、Hashtable等。这些类中的一些方法加上了 synchronized 关键字,容易误导。所以基本上来说vector 因为线程安全的实现方法比较粗暴效率较低。Vector类中大多数方法都使用synchronized关键字修饰,所以性能上vector差很多。Vector的空间扩容是一倍,内存不可复用,而ArrayList是一半 。Vector分配内存是连续的存储空间,因此我们不推荐使用vector。
使用Collections.synchronizedList:
import java.util.*;public class CopyOnWrite_List { public static void main(String[] args) { List<Object> list = Collections.synchronizedList(new ArrayList<>()); for (int i = 1; i <= 10; i++) { new Thread(()->{ list.add(UUID.randomUUID().toString().substring(0,5)); System.out.println(list); },String.valueOf(i)).start(); } }}运行结果:
同样的,使用Collections.synchronizedList一样能完成同步操作,避免同步修改异常。我们打开官方文档,查看这个方法:
可以看到,还是需要使用同步代码块来进行同步操作。
这两种方式都是普通的同步处理方式,ArrayList并不是一个线程安全的容器,对于高并发下可以用Vector,或者使用Collections的静态方法将ArrayList包装成一个线程安全的类,但是这些方式都是采用Java关键字synchronzied对方法进行修饰,利用独占式锁来保证线程安全的。但是,由于独占式锁在同一时刻只有一个线程能够获取到对象监视器,很显然这种方式效率并不是太高。而我们需要使用JUC来实现。
点开官方文档,找到JUC包
运行结果:
使用JUC下的CopyOnWriteArrayList对象来进行多线程环境下的添加,一样可以实现正常的同步操作。我们查看一下其源码:
/** The array, accessed only via getArray/setArray. */private transient volatile Object[] array;/** * Gets the array. Non-private so as to also be accessible * from CopyOnWriteArraySet class. */final Object[] getArray() { return array;}/** * Sets the array. */final void setArray(Object[] a) { array = a;}这里volatile会在之后进行学习,我们重点来分析一下CopyOnWrite,翻译过来就是写入时复制,简称COW,是计算机程序设计领域的一种优化策略。
多个线程在调用list的时候,进行读取操作时都是固定的,可是进行写入操作时,会存在覆盖问题。而COW方式能够避免这种问题。我们查看一下CopyOnWriteArrayList关于add方法的源码:
可以看到,CopyOnWriteArrayList针对写操作,比如向容器中添加一个元素,是首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。采用ReentrantLock,保证同一时刻只有一个写线程正在进行数组的复制,否则的话内存中会有多份被复制的数据;由于在写数据的时候,是在新的数组中插入数据的,从而保证读写实在两个不同的数据容器中进行操作。
采用这种读写分离的方式,能够有效的避免高并发下的线程安全问题。