首页 > 编程知识 正文

arraylist特点,arraylist的使用

时间:2023-05-05 05:16:36 阅读:165136 作者:3503

以前在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包

import java.util.*;import java.util.concurrent.CopyOnWriteArrayList;public class CopyOnWrite_List { public static void main(String[] args) { List<Object> list = new CopyOnWriteArrayList<>(); 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(); } }}

运行结果:

使用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方法的源码:

public boolean add(E e) { final ReentrantLock lock = this.lock; //1. 使用Lock,保证写线程在同一时刻只有一个 lock.lock(); try { //2. 获取旧数组引用 Object[] elements = getArray(); int len = elements.length; //3. 创建新的数组,并将旧数组的数据复制到新数组中 Object[] newElements = Arrays.copyOf(elements, len + 1); //4. 往新数组中添加新的数据 newElements[len] = e; //5. 将旧数组引用指向新的数组 setArray(newElements); return true; } finally { lock.unlock(); }}

可以看到,CopyOnWriteArrayList针对写操作,比如向容器中添加一个元素,是首先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。采用ReentrantLock,保证同一时刻只有一个写线程正在进行数组的复制,否则的话内存中会有多份被复制的数据;由于在写数据的时候,是在新的数组中插入数据的,从而保证读写实在两个不同的数据容器中进行操作。

采用这种读写分离的方式,能够有效的避免高并发下的线程安全问题。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。