数据结构(顺序表)的应用—— java并发容器之CopyOnWriteArrayList

Copy-On-Write简称COW,是一种用于程序设计中的优化策略。其基本思路是,从一开始大家都在共享同一个内容,当某个人想要修改这个内容的时候,才会真正把内容Copy出去形成一个新的内容然后再改,这是一种延时懒惰策略。从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。

(一)什么是CopyOnWrite容器?

CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

(二)CopyOnWriteArrayList如何做到线程安全的?

CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。

当有新元素加入的时候,如下图,创建新数组,并往新数组中加入一个新元素,这个时候,array这个引用仍然是指向原数组的。

当元素在新数组添加成功后,将array这个引用指向新数组。

(三)CopyOnWriteArrayList源码分析

1.继承与接口的关系关系,由此我们可以看出CopyOnWriteArrayList其实和一般的List集合实现类一样都实现了基本的接口。

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, Serializable 

2.属性字段

 private transient volatile Object[] elements;

volatile原理:

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。

volatile 性能:

volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。

3.构造函数

public CopyOnWriteArrayList() {
    elements = EmptyArray.OBJECT;  //创建一个Object类型的空表
}

 //将集合c复制到CopyOnWriteArrayList中
@SuppressWarnings("unchecked")  
public CopyOnWriteArrayList(Collection<? extends E> collection) {
    this((E[]) collection.toArray());
}

  //将传入的数组复制到CopyOnWriteArrayList中
 public CopyOnWriteArrayList(E[] array) {
    this.elements = Arrays.copyOf(array, array.length, Object[].class);
}

4.增删改查操作
(1)添加操作

public synchronized boolean add(E e) {
    Object[] newElements = new Object[elements.length + 1];  //这里是重新构造了一个新的数组
    System.arraycopy(elements, 0, newElements, 0, elements.length);
    newElements[elements.length] = e;
    elements = newElements;    //然后再把新的数组复制给原来的数组
    return true;
}

CopyOnWriteArrayList的整个add操作都是在锁的保护下进行的。 这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了。下面的几个add方法也是同理。

public synchronized void add(int index, E e) {
    Object[] newElements = new Object[elements.length + 1];
    System.arraycopy(elements, 0, newElements, 0, index);
    newElements[index] = e;
    System.arraycopy(elements, index, newElements, index + 1, elements.length - index);
    elements = newElements;
}

public synchronized boolean addAll(Collection<? extends E> collection) {
    return addAll(elements.length, collection);
}

public synchronized boolean addAll(int index, Collection<? extends E> collection) {
    Object[] toAdd = collection.toArray();
    Object[] newElements = new Object[elements.length + toAdd.length];
    System.arraycopy(elements, 0, newElements, 0, index);
    System.arraycopy(toAdd, 0, newElements, index, toAdd.length);
    System.arraycopy(elements, index,
            newElements, index + toAdd.length, elements.length - index);
    elements = newElements;
    return toAdd.length > 0;
}

由于所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:
1、如果写操作未完成,那么直接读取原数组的数据;
2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

可见,CopyOnWriteArrayList的读操作是可以不用加锁的。

(2)删除操作,也是开辟另一个数组来操作

public synchronized E remove(int index) {
    @SuppressWarnings("unchecked")
    E removed = (E) elements[index];
    removeRange(index, index + 1);
    return removed;
}

private void removeRange(int from, int to) {
Object[] newElements = new Object[elements.length - (to - from)];
System.arraycopy(elements, 0, newElements, 0, from);
System.arraycopy(elements, to, newElements, from, elements.length - to);
elements = newElements;
}

public synchronized boolean remove(Object o) {
    int index = indexOf(o);
    if (index == -1) {
        return false;
    }
    remove(index);
    return true;
}

public synchronized boolean removeAll(Collection<?> collection) {
    return removeOrRetain(collection, false, 0, elements.length) != 0;
}

public synchronized boolean retainAll(Collection<?> collection) {
    return removeOrRetain(collection, true, 0, elements.length) != 0;
}

(3)修改操作

public synchronized E set(int index, E e) {
    Object[] newElements = elements.clone();
    @SuppressWarnings("unchecked")
    E result = (E) newElements[index];
    newElements[index] = e;
    elements = newElements;
    return result;
}

(4)读取操作

@SuppressWarnings("unchecked")
public E get(int index) {
    return (E) elements[index];
}

读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。

(5)其他的集合方法基本和ArrayList一样了,就不在继续分析了。

通过上面的分析,CopyOnWriteArrayList 有几个缺点:
1、由于写操作的时候,存在内存占用的问题,因为每次对容器结构进行修改的时候都要对容器进行复制,这么一来我们就有了旧有对象和新入的对象,会占用两份内存。如果对象占用的内存较大,就会引发频繁的垃圾回收行为,降低性能;

2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;

CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用。因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

CopyOnWriteArrayList透露的思想,如上面的分析CopyOnWriteArrayList表达的一些思想:
1、读写分离,读和写分开
2、最终一致性 (CopyOnWrite只能保证数据最终的一致性,不能保证数据的实时一致性。)
3、使用另外开辟空间的思路,来解决并发冲突.

CopyOnWriteArrayList对我们还说还是了解有这么一个东西存在而已,一般Android都不会使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容