使用多线程往LIST添加数据 线程安全list CopyOnWriteArrayList与Collections.synchronizedList的性能对比

列表实现有ArrayList、Vector、CopyOnWriteArrayList、Collections.synchronizedList(list)四种方式。

1 ArrayList

        ArrayList是非线性安全,此类的 iterator 和 listIterator 方法返回的迭代器是快速失败的:在创建迭代器之后,除非通过迭代器自身的 remove 或 add 方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出 ConcurrentModificationException。即在一方在便利列表,而另一方在修改列表时,会报ConcurrentModificationException错误。而这不是唯一的并发时容易发生的错误,在多线程进行插入操作时,由于没有进行同步操作,容易丢失数据。

[java] view plain copy  

public boolean add(E e) {  

ensureCapacity(size +1);  // Increments modCount!!  

elementData[size++] = e;//使用了size++操作,会产生多线程数据丢失问题。  

return true;  

   }  

因此,在开发过程当中,ArrayList并不适用于多线程的操作。

2 Vector

从JDK1.0开始,Vector便存在JDK中,Vector是一个线程安全的列表,采用数组实现。其线程安全的实现方式是对所有操作都加上了synchronized关键字,这种方式严重影响效率,因此,不再推荐使用Vector了,Stackoverflow当中有这样的描述: Why is Java Vector class considered obsolete or deprecated?

3 Collections.synchronizedList & CopyOnWriteArrayList

CopyOnWriteArrayList和Collections.synchronizedList是实现线程安全的列表的两种方式。两种实现方式分别针对不同情况有不同的性能表现,其中CopyOnWriteArrayList的写操作性能较差,而多线程的读操作性能较好。而Collections.synchronizedList的写操作性能比CopyOnWriteArrayList在多线程操作的情况下要好很多,而读操作因为是采用了synchronized关键字的方式,其读操作性能并不如CopyOnWriteArrayList。因此在不同的应用场景下,应该选择不同的多线程安全实现类。 

3.1 Collections.synchronizedList

Collections.synchronizedList的源码可知,其实现线程安全的方式是建立了list的包装类,代码如下: 

[java] view plain copy  

public static <T> List<T> synchronizedList(List<T> list) {  

return (list instanceof RandomAccess ?  

new SynchronizedRandomAccessList<T>(list) :  

new SynchronizedList<T>(list));//根据不同的list类型最终实现不同的包装类。  

   }  

其中,SynchronizedList对部分操作加上了synchronized关键字以保证线程安全。但其iterator()操作还不是线程安全的。部分SynchronizedList的代码如下:

[java] view plain copy  

public E get(int index) {  

synchronized(mutex) {return list.get(index);}  

        }  

public E set(int index, E element) {  

synchronized(mutex) {return list.set(index, element);}  

        }  

public void add(int index, E element) {  

synchronized(mutex) {list.add(index, element);}  

        }  

public ListIterator<E> listIterator() {  

return list.listIterator(); // Must be manually synched by user 需要用户保证同步,否则仍然可能抛出ConcurrentModificationException  

        }  


public ListIterator<E> listIterator(int index) {  

return list.listIterator(index); // Must be manually synched by user <span style="font-family: Arial, Helvetica, sans-serif;">需要用户保证同步,否则仍然可能抛出ConcurrentModificationException</span>  

        }  

3.2 CopyOnWriteArrayList

        从字面可以知道,CopyOnWriteArrayList在线程对其进行些操作的时候,会拷贝一个新的数组以存放新的字段。其写操作的代码如下:

[java] view plain copy  

/** The lock protecting all mutators */  

transient final ReentrantLock lock = new ReentrantLock();  


/** The array, accessed only via getArray/setArray. */  

private volatile transient Object[] array;//保证了线程的可见性  


public boolean add(E e) {  

final ReentrantLock lock = this.lock;//ReentrantLock 保证了线程的可见性和顺序性,即保证了多线程安全。  

    lock.lock();  

try {  

        Object[] elements = getArray();  

int len = elements.length;  

Object[] newElements = Arrays.copyOf(elements, len +1);//在原先数组基础之上新建长度+1的数组,并将原先数组当中的内容拷贝到新数组当中。  

newElements[len] = e;//设值  

setArray(newElements);//对新数组进行赋值  

return true;  

}finally {  

        lock.unlock();  

    }  

    }  

其读操作代码如下:

[java] view plain copy  

public E get(int index) {  

return (E)(getArray()[index]);  

   }  

其没有加任何同步关键字,根据以上写操作的代码可知,其每次写操作都会进行一次数组复制操作,然后对新复制的数组进行些操作,不可能存在在同时又读写操作在同一个数组上( 不是同一个对象),而读操作并没有对数组修改,不会产生线程安全问题。Java中两个不同的引用指向同一个对象,当第一个引用指向另外一个对象时,第二个引用还将保持原来的对象。 

其中setArray()操作仅仅是对array进行引用赋值。Java中“=”操作只是将引用和某个对象关联,假如同时有一个线程将引用指向另外一个对象,一个线程获取这个引用指向的对象,那么他们之间不会发生ConcurrentModificationException,他们是在虚拟机层面阻塞的,而且速度非常快,是一个原子操作,几乎不需要CPU时间。

        在列表有更新时直接将原有的列表复制一份,并再新的列表上进行更新操作,完成后再将引用移到新的列表上。旧列表如果仍在使用中(比如遍历)则继续有效。如此一来就不会出现修改了正在使用的对象的情况(读和写分别发生在两个对象上),同时读操作也不必等待写操作的完成,免去了锁的使用加快了读取速度。

3.3 Collections.synchronizedList & CopyOnWriteArrayList在读写操作上的差距

        测试代码:

[java] view plain copy  

package com.yang.test;  


import org.junit.Test;  


import java.util.*;  

import java.util.concurrent.*;  


/**

 * Created with IntelliJ IDEA.

 * User: yangzl2008

 * Date: 14-9-18

 * Time: 下午8:36

 * To change this template use File | Settings | File Templates.

 */  

public class Test02 {  


private int NUM = 10000;  

private int THREAD_COUNT = 16;  


@Test  

public void testAdd() throws Exception {  

List list1 =new CopyOnWriteArrayList<Integer>();  

List list2 = Collections.synchronizedList(new ArrayList<Integer>());  

Vector v  =new Vector<Integer>();  


CountDownLatch add_countDownLatch =new CountDownLatch(THREAD_COUNT);  

        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);  


int add_copyCostTime = 0;  

int add_synchCostTime = 0;  

for (int i = 0; i < THREAD_COUNT; i++) {  

add_copyCostTime += executor.submit(new AddTestTask(list1, add_countDownLatch)).get();  

        }  

System.out.println("CopyOnWriteArrayList add method cost time is " + add_copyCostTime);  


for (int i = 0; i < THREAD_COUNT; i++) {  

add_synchCostTime += executor.submit(new AddTestTask(list2, add_countDownLatch)).get();  

        }  

System.out.println("Collections.synchronizedList add method cost time is " + add_synchCostTime);  



    }  


@Test  

public void testGet() throws Exception {  

        List<Integer> list = initList();  


List list1 =new CopyOnWriteArrayList<Integer>(list);  

        List<Integer> list2 = Collections.synchronizedList(list);  


int get_copyCostTime = 0;  

int get_synchCostTime = 0;  

        ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);  

CountDownLatch get_countDownLatch =new CountDownLatch(THREAD_COUNT);  

for (int i = 0; i < THREAD_COUNT; i++) {  

get_copyCostTime += executor.submit(new GetTestTask(list1, get_countDownLatch)).get();  

        }  

System.out.println("CopyOnWriteArrayList add method cost time is " + get_copyCostTime);  


for (int i = 0; i < THREAD_COUNT; i++) {  

get_synchCostTime += executor.submit(new GetTestTask(list2, get_countDownLatch)).get();  

        }  

System.out.println("Collections.synchronizedList add method cost time is " + get_synchCostTime);  


    }  



private List<Integer> initList() {  

List list =new ArrayList<Integer>();  

int num = new Random().nextInt(1000);  

for (int i = 0; i < NUM; i++) {  

            list.add(num);  

        }  

return list;  

    }  


class AddTestTask implements Callable<Integer> {  

        List<Integer> list;  

        CountDownLatch countDownLatch;  


        AddTestTask(List<Integer> list, CountDownLatch countDownLatch) {  

this.list = list;  

this.countDownLatch = countDownLatch;  

        }  


@Override  

public Integer call() throws Exception {  

int num = new Random().nextInt(1000);  

long start = System.currentTimeMillis();  

for (int i = 0; i < NUM; i++) {  

                list.add(num);  

            }  

long end = System.currentTimeMillis();  

            countDownLatch.countDown();  

return (int) (end - start);  

        }  

    }  


class GetTestTask implements Callable<Integer> {  

        List<Integer> list;  

        CountDownLatch countDownLatch;  


        GetTestTask(List<Integer> list, CountDownLatch countDownLatch) {  

this.list = list;  

this.countDownLatch = countDownLatch;  

        }  


@Override  

public Integer call() throws Exception {  

int pos = new Random().nextInt(NUM);  

long start = System.currentTimeMillis();  

for (int i = 0; i < NUM; i++) {  

                list.get(pos);  

            }  

long end = System.currentTimeMillis();  

            countDownLatch.countDown();  

return (int) (end - start);  

        }  

    }  

}  

操作结果:

 写操作读操作

 CopyOnWriteArrayListCollections.

synchronizedList

CopyOnWriteArrayListCollections.

synchronizedList

2567211

43088322

8259752823

162959364426

32--38

64--721

128--938

写操作:在线程数目增加时CopyOnWriteArrayList的写操作性能下降非常严重,而Collections.synchronizedList虽然有性能的降低,但下降并不明显。

        读操作:在多线程进行读时,Collections.synchronizedList和CopyOnWriteArrayList均有性能的降低,但是Collections.synchronizedList的性能降低更加显著。

4 结论

CopyOnWriteArrayList,发生修改时候做copy,新老版本分离,保证读的高性能,适用于以读为主,读操作远远大于写操作的场景中使用,比如缓存。而Collections.synchronizedList则可以用在CopyOnWriteArrayList不适用,但是有需要同步列表的地方, 读写操作都比较均匀的地方。

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

推荐阅读更多精彩内容