Chapter 12 CopyOnWriteArrayList原理分析

1 概述

JDK1.5的j.u.c库中提供了Copy-On-Write机制,简称为COW。其实现的基本思想是,开始的时候只有一份内容,当某个线程想要修改这个内容的时候,会将其拷贝出去形成一份新的内容,在这份新的内容上进行修改,这是一种延时懒惰策略。用该机制形成的有两个并发容器,即CopyOnWriteArrayList和CopyOnWriteArraySet。其中,后者是基于前者实现的,当进行数据的添加和删除时是在数组上进行遍历的,这和HashSet的哈希表实现以及TreeSet的二叉树实现是不同的,所以其效率是比较低效的,只是实现了元素的唯一性。

这里主要讨论一下CopyOnWriteArrayList的实现原理。

2 CopyOnWriteArrayList性能测试

为了测试CopyOnWriteArrayList的性能,这里将普通的ArrayList与其一起进行测试。测试的过程是,同时开启10个写线程和10个读线程对同一个list进行操作,具体的测试代码如下:

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
/**
 * Created by fubinhe on 16/12/1.
 */
public class CopyOnWriteArrayListDemo {
 
    public static final int NUM = 10;
 
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < NUM; ++i) {
            list.add("main_" + i);
        }
        list.get(0);
        ExecutorService exec = Executors.newCachedThreadPool();
        for (int i = 0; i < NUM; ++i) {
            exec.execute(new WriteTask(list, i));
            exec.execute(new ReadTask(list));
        }
        exec.shutdown();
    }
}
 
class WriteTask implements Runnable {
 
    private List<String> list;
 
    private int removeIndex;
 
    public WriteTask(List<String> list, int removeIndex) {
        this.list = list;
        this.removeIndex = removeIndex;
    }
 
    @Override
    public void run() {
        list.remove(removeIndex);
        list.add("write_" + removeIndex);
    }
}
 
class ReadTask implements Runnable {
 
    private List<String> list;
 
    public ReadTask(List<String> list) {
        this.list = list;
    }
 
    @Override
    public void run() {
        for (String s : list) {
            System.out.println(s);
        }
    }
}
运行上述代码,程序将抛出ConcurrentModificationException异常。这是因为,在读线程试图读取下一个元素的时候,该元素恰好被写线程删除了,迭代器失效。如果将第16行代码替换成下面的代码,则可以顺利的执行并发的读写。

List<String> list = new CopyOnWriteArrayList<>();

3 源代码分析

CopyOnWriteArrayList之所以能进行并发的读写,是因为采用了读写分离的思想,读和写是在不用的容器中进行的。下面分析一些其核心方法的源代码。

3.1 add方法

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();
    }
}

3.2 remove方法

和普通ArrayList类似,remove方法有两种形式,即删除某个索引的值和删除某个对象,这里讨论一下第一种形式,其源代码如下:

public E remove(int index) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        if (numMoved == 0)
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            Object[] newElements = new Object[len - 1];
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}

和add方法类似,其实现方式也比较简单,以某个索引为界分两次将原数组的值拷贝到新数组中。

3.3 get方法

CopyOnWriteArrayList在执行读操作时不进行加锁,所以其实现更简单,直接返回数组中某个索引的值。由于没有加锁,所以如果在读的同时有写线程对数组进行操作,那么有可能读到的值并不是最新的。

public E get(int index) {
    return get(getArray(), index);
}
  
private E get(Object[] a, int index) {
    return (E) a[index];
}

4 总结

COW机制使用读写分离的思想提高并发性能,使得多个读写操作能同时进行。但是很明显,其也有两个缺点,即:

(1)由于读操作没有加锁,所以如果在读的过程中也有写操作在进行,那么无法保证实时的一致性,只能做到最终的一致性。

(2)占用内存多。因为在对数组进行修改时,需要将原有的数组拷贝到新的数组中,那么内存中会多维护一份数组。如果修改的操作频繁,会造成比正常更多的FULL GC。





  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值