CopyOnWriteArrayList详解

先说

先来一张jdk1.7的api截图

如上图,看List的几个常用实现类。

ArrayList、LinkedList和Vector来做区别:


1.ArrayList和LinkedList底层核心:ArrayList是实现了基于动态数组的数据结构,LinkedList基于链表的数据结构。那么他们俩的增、删、改、查等操作的 性能 差别就参照 数组 与 链表 的差别。


2.ArrayList与Vector都是可动态增长空间。但是Vector每次增一倍左右,ArrayList增一半左右。


3.ArrayList和LinkedList是非线程安全的,Vector是线程安全的。而Vector线程安全机制是通过锁容器(比如数据库中锁整张表)实现,这就必然影响性能。所以Vector的使用场景越来越少。


以上的几点区别都通过源码可以清晰的得知,这里就不繁琐贴上源码。


为什么说到这几个,因为在多线程编程中,LinkedList和ArrayList的线程安全问题不可不考虑。有兴趣的尽可以做一个测试:A线程往ArrayList或LinkedList中插入数据,B线程遍历容器中的数据,如下:

	public static void main(String[] args) {
		final ArrayList<Integer> a = new ArrayList<>();
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				int i = 0;
				while(i < 10000){
					a.add(i++);
				}
			}
		}).start();
		
		new Thread(new Runnable() {
					
			@Override
			public void run() {
				for(Integer i:a){
					System.out.println(i);
				}
			}
		}).start();
	}
运行结果:

Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
	at java.util.ArrayList$Itr.next(ArrayList.java:831)
	at B$2.run(B.java:63)
	at java.lang.Thread.run(Thread.java:745)
报错原因就是一个线程在读一个正在被修改的容器中的数据。有人说因为用的是foreach,那么就用迭代器Iterator试一下:

把第二个线程的执行内容换成:

		new Thread(new Runnable() {
					
			@Override
			public void run() {
				Iterator<Integer> iterator = a.iterator();
				while(iterator.hasNext()){
					System.out.println(iterator.next());
				}
			}
		}).start();
执行结果:会出现几种情况:(1)什么都没有打印。(2)直接抛出和之前同样异常。(3)打印一段之后抛出之前同样异常

我这里放出第三种:

1
.
.
16
17
Exception in thread "Thread-1" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:859)
	at java.util.ArrayList$Itr.next(ArrayList.java:831)
	at B$2.run(B.java:39)
	at java.lang.Thread.run(Thread.java:745)

使用迭代器确实会线程安全的去迭代数据,但是他的线程安全也是加锁,也就是说出现第一种情况什么都没有打印,是因为ArrayList被锁住了,无法添加。出现(2)(3)情况则是因为迭代器在第一个线程add后执行。


那么总之,这就是线程安全的问题,此时可以想到java.util.concurrent这个并发包

java.util.concurrent 包里没有并发的ArrayList实现

查看jdk文档,发现并没有对应ArrayList的并发实现,而HashMap却有ConcurrentHashMap并发实现。为什么在java.util.concurrent 没有一个类可以代替Vector的线程安全呢?

个人认为:

ConcurrentHashMap的出现更多的在于保证并发,从它采用了锁分段技术和弱一致性的Map迭代器去避免并发瓶颈可知。而ArrayList中很多操作很难避免锁整表,就如contains()、随机取get()等,进行查询搜索时都是要整张表操作的,那多线程时数据的实时一致性就只能通过锁来保证,这就限制了并发。

那么多线程操作时,就不能使用ArrayList和LinkedList。这就有了CopyOnWriteArrayList

CopyOnWriteArrayList的优与缺


api原文说明:


A thread-safe variant of ArrayList in which all mutative operations (add, set, and so on) are implemented by making a fresh copy of the underlying array.

This is ordinarily too costly, but may be more efficient than alternatives when traversal operations vastly outnumber mutations, and is useful when you cannot or don't want to synchronize traversals, yet need to preclude interference among concurrent threads. The "snapshot" style iterator method uses a reference to the state of the array at the point that the iterator was created. This array never changes during the lifetime of the iterator, so interference is impossible and the iterator is guaranteed not to throw ConcurrentModificationException. The iterator will not reflect additions, removals, or changes to the list since the iterator was created. Element-changing operations on iterators themselves (remove,set, and add) are not supported. These methods throw UnsupportedOperationException.

All elements are permitted, including null.
Memory consistency effects: As with other concurrent collections, actions in a thread prior to placing an object into aCopyOnWriteArrayList happen-before actions subsequent to the access or removal of that element from theCopyOnWriteArrayList in another thread



简单翻译:

ArrayList的线程安全变体,其中增删改都是通过对底层数组的新拷贝实现的。

这通常开销太大,但是当遍历操作远远超过修改、插入时,可能比其他方法更有效,当您不能或不想同步遍历时,这是很有用的,但是需要排除并发线程之间的干扰“快照”风格的迭代器方法在创建迭代器的时候使用一个指向数组状态的引用。这个数组迭代器的生命周期期间从未改变,所以干涉是不可能的,迭代器是可以保证不抛出ConcurrentModificationException异常。迭代器不会在创建迭代器时反映添加、删除或更改。不支持对迭代器本身进行元素更改操作(删除、设置和添加)。这些方法抛出UnsupportedOperationException。

允许null。

内存一致性影响 : 与其他并发集合一样,会发生对象被放置到CopyOnWriteArrayList之前的线程中,然后在另一个线程中从CopyOnWriteArrayList中访问或删除该元素之前的操作(线程修改时,其他线程并不能访问到最新的数据)

总结以上说明:要对原容器A做增删改,就先拷贝一份为B,在B中做增、删、改。此时其他线程读的是A的数据。修改完成之后把指向A的引用改变指向到B,这样完成操作。

他的具体操作和实现原理,在这是很难讲清,最好的说明就是api或看源码。

那可能会想到一个情景:两个线程同时对容器修改,该如何实现?如下测试,看能否修改成功:

	public static void main(String[] args) {
		final CopyOnWriteArrayList<Integer> a = new CopyOnWriteArrayList<>();
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				int i = 0;
				while(i < 5){
					a.add(i++);
					System.out.println("t1进行了添加,测试容器长度:"+a.size());
				}
			}
		}).start();
		new Thread(new Runnable() {
					
			@Override
			public void run() {
				int i = 0;
				while(i < 5){
					a.add(i++);
					System.out.println("t2进行了添加,测试容器长度:"+a.size());
				}
			}
		}).start();
	}
其中一组执行输出:

t2进行了添加,测试容器长度:2
t1进行了添加,测试容器长度:2
t2进行了添加,测试容器长度:3
t1进行了添加,测试容器长度:4
t2进行了添加,测试容器长度:5
t1进行了添加,测试容器长度:6
t2进行了添加,测试容器长度:7
t1进行了添加,测试容器长度:8
t2进行了添加,测试容器长度:9
t1进行了添加,测试容器长度:10
可知最后两个线程都执行成功,但是他们的执行细节是怎样的?
查看源码add()的方法:

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
可知各个操作之间通过并发锁来保证 【写】的一致性,先复制一份,然后添加到最后,然后修改指向。

优缺点

其实api文档说的明确了。开销和读写安全

优点:

1.保证多线程的并发读写的线程安全

缺点:

内存:有数组拷贝自然有内存问题。如果实际应用数据比较多,而且比较大的情况下,占用内存会比较大,这个可以用ConcurrentHashMap来代替。

数据一致性:CopyOnWrite容器只能保证数据的最终一致性不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器

总结CopyOnWriteArrayList

根据api的介绍说明,应用场景:遍历操作远多于修改、内存开销可忽略



  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
CopyOnWriteArrayList是Java中的一个线程安全的List实现。它实现了List、RandomAccess、Cloneable和Serializable等接口,并且对并发访问做了优化。 在CopyOnWriteArrayList中,如果要将一个非CopyOnWriteArrayList类型的List对象c拷贝到当前List的数组中,会进行拷贝操作,即将c的元素全部拷贝到当前List的数组中。这个操作是通过调用构造函数CopyOnWriteArrayList(E[] toCopyIn)来实现的,内部使用Arrays.copyOf方法进行拷贝操作。 CopyOnWriteArrayList的add(E e)方法用于向列表中添加元素e。在添加元素时,会进行一次数组的拷贝,确保线程安全性。 与其他List不同,CopyOnWriteArrayList在遍历时不会抛出ConcurrentModificationException异常。这是因为CopyOnWriteArrayList在遍历时是对原始数组进行遍历,而不是对拷贝出来的数组进行遍历。因此,在遍历过程中对列表进行修改不会影响当前遍历的结果。 总结来说,CopyOnWriteArrayList是一个线程安全的List实现,通过拷贝数组的方式实现并发访问的安全性,避免了ConcurrentModificationException异常。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [【JAVA集合篇】CopyOnWriteArrayList详解](https://blog.csdn.net/jiang_wang01/article/details/131257609)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [Java中CopyOnWriteArrayList的使用](https://download.csdn.net/download/weixin_38728555/14911703)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值