前言

  之前看《Java并发编程》这本书的时候,有看到这个,只记得"读多写少"、"写入时复制"。书中没有过多讲述,只是一笔带过(不过现在回头看,发现讲的都是精髓。老外的书大多重理论,喜欢花大篇幅讲概念,这点我非常喜欢)记得当时是觉得可能有点难,先跳过了,结果就忘记回头看了。今天突然想起来,就看了一下,整理一点东西。

非线程安全的ArrayList

我们知道原来util包中的ArrayList是不提供同步的,也就是说当多个线程读写ArrayList的时候可能出现线程安全问题。例如,就add操作而言

ArrayList的add方法:

我们把elementData[size++] = e 分为两步:

  • elementData[size]=e
  • size++

如果调用两次add操作,期待结果应该是这样的:

但是,如果存在两个线程A、B几乎同时操作add方法,由于无法保证add操作的原子性,实际操作时序可能如下。

那么,对应的结果就会是这样:

这里就发生大问题了,e1被后续添加的e2覆盖。e1丢失,而size却仍旧递增两位。

线程安全的ArrayList——SynchronizedList

Collections实用类(注意,不是Collection接口)提供同步容器包装,将普通的集合包装成线程安全的集合。

例如,通过Collections.synchronizedList(List<T>)方法,可以把一个非线程安全的List集合变为线程安全的集合。

这里其实是一个装饰器模式的应用,参数集合List将被装饰为SynchronizedList。

其是Collections的内部类。

通过对每个方法调用都进行同步加锁,使得多个线程读写ArrayList只能按序进行。这样的话,数据的安全性和一致性都得到了保证。

缺点也非常明显,每个线程读写ArrayList都需进行同步,开销大。

迭代过程中的异常 —— ConcurrentModificationException 

在ArrayList的实现中,其迭代器实现了一个方法checkForComodification 这个方法会检查迭代期间是否有其他线程修改了集合,如果有,则抛出ConcurrentModificationException

原理

主要跟两个字段有关:

  • expectedModCount(来自ArrayList的迭代器Itr)
  • modCount(来自ArrayList的父类AbstractList,初始为0)。

ArrayList实现中,每执行一次添加操作,都会让modCount+1

注意:addAll也是让modCount+1,与添加的元素个数无关。remove和set操作不算,其不会让modCount有所改变。

ArrayList的迭代器中,expectedModCount的初始值被设定为modCount

迭代器在每次遍历时,会调用checkForComodification 检查状态,如果此过程中集合发生了改动,则直接抛出异常。

注:如果迭代期间需要修改集合,只能通过迭代器的方法修改集合,这些方法不会触发异常。因为其会重置expectedModCount的值为当前modCount。

如何防止迭代过程出现异常?

所以,在使用这样的ArrayList时,如果需要对其进行迭代,则需要对容器进行加锁(或者拷贝一份),使当前线程对其独占访问,以保证其迭代过程能够正常运行。如下:

public class SomeClass {
List<E> list; public SomeClass(List<E> list) {
this.list = list;
} //如果这个方法会被多线程访问,那么最好对list的访问进行加锁
public void function() {
synchronized(list) {
for(E e:list) {
....
}
}
}
}

线程安全的另一种实现类——CopyOnWriteArrayList

CopyOnWriteArrayList同样是线程安全的ArrayList,但是与SynchronizedList不同的是,它只对写操作加锁,对读操作不加锁。关键是,其在迭代期间不需要对容器进行加锁或复制。这一切都与"写入时复制"有关。

写入时复制的原理

CopyOnWriteArrayList的字段:

  • lock => 锁,写操作时需要
  • array => 容器数组的引用。指向存储当前元素的数组。

与容器数组引用直接相关的两个方法:

写入时复制相关代码:

public boolean add(E e) {
final ReentrantLock lock = this.lock;
//写操作还是要上锁的,此锁是全局锁
lock.lock();
try {
//获取容器数组
Object[] elements = getArray();
//获得容器长度
int len = elements.length;
//创建一个新的存储空间,容量+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//在新的存储空间内,加入新元素
newElements[len] = e;
//修改当前容器数组的引用
setArray(newElements);
return true;
} finally {
//释放锁
lock.unlock();
}
}

当add操作完成后,array的引用就已经指向另一个存储空间了。 这里也暴露了一个缺点:如果此容器的写操作比较频繁,那么其开销就比较大。

迭代器实现

CopyOnWriteArray有自己的迭代器,该迭代器不会检查修改状态,也无需检查状态。因为迭代的数组是可以说是只读的,不会有其他线程能够修改它。

迭代器,引用的数组变量名就叫snapshot(快照)。也从另一个角度说明,在迭代器迭代过程中,其使用的是容器的过去一个版本,一个快照。不能保证是当前容器的状态。

这里也暴露了一个缺点:不能保证数据的瞬时一致性。

但是,其有一个显著的优点,那就是读操作,和遍历操作不需要同步。多线程访问的时候,速度较高。

CopyOnWriteArrayList应用场景

  由以上的优缺点可得,CopyOnWriteArrayList应用的场景,最好是读操作多,写操作相对较少的场景("读多写少")。也就是说,集合内容不会经常变动的。例如,网上常说的"黑名单"这类东西。

【杂谈】对CopyOnWriteArrayList的认识的更多相关文章

  1. 【JUC】JDK1.8源码分析之CopyOnWriteArrayList(六)

    一.前言 由于Deque与Queue有很大的相似性,Deque为双端队列,队列头部和尾部都可以进行入队列和出队列的操作,所以不再介绍Deque,感兴趣的读者可以自行阅读源码,相信偶了Queue源码的分 ...

  2. 【转】PHP 杂谈《重构-改善既有代码的设计》之一 重新组织你的函数

    原文地址: PHP 杂谈<重构-改善既有代码的设计>之一 重新组织你的函数 思维导图   点击下图,可以看大图.    介绍   我把我比较喜欢的和比较关注的地方写下来和大家分享.上次我写 ...

  3. JAVA 多线程随笔 (三) 多线程用到的并发容器 (ConcurrentHashMap,CopyOnWriteArrayList, CopyOnWriteArraySet)

    1.引言 在多线程的环境中,如果想要使用容器类,就需要注意所使用的容器类是否是线程安全的.在最早开始,人们一般都在使用同步容器(Vector,HashTable),其基本的原理,就是针对容器的每一个操 ...

  4. Java CopyOnWriteArrayList

    1. 为什么需要 CopyOnWriteArrayList ArrayList 的内部实现是一个数组, 并且是动态扩容的, 当插入数据时, 先判断数组是否需要扩容, 如果需要扩容, 则先扩容, 再插入 ...

  5. 图解集合3:CopyOnWriteArrayList

    初识CopyOnWriteArrayList 第一次见到CopyOnWriteArrayList,是在研究JDBC的时候,每一个数据库的Driver都是维护在一个CopyOnWriteArrayLis ...

  6. 【管理心得之三十二】PMP杂谈---------爱情必胜术

    这次一反常态,没有场景设计,我想借此文普及一下PMP是什么? 但我不知道这样枯燥的话题能否能引起你的兴趣,我不得不套用“标题党”<爱情必胜术>来博你眼球. 我真没有说谎,此文是献给那些孤身 ...

  7. 如何线程安全地遍历List:Vector、CopyOnWriteArrayList

    遍历List的多种方式 在讲如何线程安全地遍历List之前,先看看通常我们遍历一个List会采用哪些方式. 方式一: for(int i = 0; i < list.size(); i++) { ...

  8. Java多线程系列--“JUC集合”02之 CopyOnWriteArrayList

    概要 本章是"JUC系列"的CopyOnWriteArrayList篇.接下来,会先对CopyOnWriteArrayList进行基本介绍,然后再说明它的原理,接着通过代码去分析, ...

  9. java并发编程:并发容器之CopyOnWriteArrayList(转)

    原文:http://ifeve.com/java-copy-on-write/ Copy-On-Write简称COW,是一种用于程序设计中的优化策略.其基本思路是,从一开大家都在共享同一个内容,当某个 ...

随机推荐

  1. GPT分区在IBM服务器上安装linux不能引导的解决方法

    提示: Your boot partition is on a disk using the GPT partitioning Scheme but this machines cannot boot ...

  2. 网络timeout区分

    ConnectTimeout 连接建立时间,三次握手完成时间 SocketTimeout 数据传输过程中数据包之间间隔的最大时间 下面重点说下SocketTimeout,比如有如下图所示的http请求 ...

  3. set_magic_quotes_runtime set_magic_quotes_gpc

    set_magic_quotes_runtime(0); 可以修改php.ini中 magic_quotes_runtime boolean的设置 当你的数据中有一些\"'这样的字符要写入到 ...

  4. java锁的种类以及辨析(转载)

    java锁的种类以及辨析(一):自旋锁 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) .这些已经写好提供的锁为我 ...

  5. ASP.NET Web API 入门 (API接口、寄宿方式、HttpClient调用)

    一.ASP.NET Web API接口定义 ASP.NET Web API默认实现了Action方法和HTTP方法的映射,Action方法方法名体现了其能处理的请求必须采用的HTTP方法 二.寄宿方式 ...

  6. unigui回车代替TAB

    unigui回车代替TAB 在业务系统中常常使用回车键(Enter)替代Tab键完成焦点跳转,在uniGUI下,可以不用代码,直接使用TUniForm的NavigateKeys进行设置: 其中Next ...

  7. 数据库常见索引解析(B树,B-树,B+树,B*树,位图索引,Hash索引)

    B树 即二叉搜索树: 1.所有非叶子结点至多拥有两个儿子(Left和Right): 2.所有结点存储一个关键字: 3.非叶子结点的左指针指向小于其关键字的子树,右指针指向大于其关键字的子树: 如: B ...

  8. Django(ORM查询联系题)

    day70 练习题:http://www.cnblogs.com/liwenzhou/articles/8337352.html import os import sys if __name__ == ...

  9. hashMap tableSizeFor 实现原理

    基于jdk1.8 hashMap实现,要求容量大小是2的整次方,例如:2/4/8/16/32/64/128...,而不能是中间的某个值.这是为什么呢? map是数组+链表的数据结构,读写数据都需要首先 ...

  10. 开发ASP.NET MVC 开发名片二维码生成工具 (原创)

    在网上找了很多,都只能生成网址,不能生成名片二维码,于是自己动手. 第一步,写视图界面,主要代码如下: <script type="text/javascript"> ...