面试官:说一下这头牛COW,Copy-On-Write

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

1、什么是CopyOnWrite容器

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

2、CopyOnWriteArrayList的实现原理

在使用CopyOnWriteArrayList之前,我们先阅读其源码了解下它是如何实现的。以下代码是向ArrayList里添加元素,可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
	//1、先加锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;	
        //2、拷贝数组
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //3、将元素加入到新数组中
        newElements[len] = e;
        //4、将array引用指向到新数组
        setArray(newElements);
        return true;
    } finally {
    	//5、解锁
        lock.unlock();
    }
}

/**
 * Sets the array.
 */
final void setArray(Object[] a) {
    array = a;
}

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

/**
 * {@inheritDoc}
 *
 * @throws IndexOutOfBoundsException {@inheritDoc}
 */
public E get(int index) {
    return get(getArray(), index);
}

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

我们测试下多线程操作CopyOnWriteArrayList:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class Test {
	
    public static void main(String[] args)  {
    	List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        
        //用CopyOnWriteArrayList处理多线程访问
        final CopyOnWriteArrayList<Integer> cowList = new CopyOnWriteArrayList<Integer>(list);
        
        Thread thread1 = new Thread(){
            public void run() {
                Iterator<Integer> iterator = cowList.iterator();
                while(iterator.hasNext()){
                    Integer integer = iterator.next();
                    System.out.println("thread1 --- "+integer);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        };
        
        Thread thread2 = new Thread(){
            public void run() {
                Iterator<Integer> iterator = cowList.iterator();
                while(iterator.hasNext()){
                    Integer integer = iterator.next();
                    if(integer==2) {
                    	cowList.remove(integer);
                    	System.out.println("thread2删除元素完毕");
                    }
                }
                
                iterator = cowList.iterator();
                while(iterator.hasNext()){
                    System.out.println("thread2 --- "+iterator.next());
                }
            };
        };
        
        Thread thread3 = new Thread(){
            public void run() {
            	 try {
                     Thread.sleep(5000);
                 } catch (InterruptedException e) {
                     e.printStackTrace();
                 }
            	 
                Iterator<Integer> iterator = cowList.iterator();
                while(iterator.hasNext()){
                    Integer integer = iterator.next();
                    System.out.println("thread3 --- "+integer);
                   
                }
            };
        };
        
        
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

运行结果如下,正常没有报异常。

thread2删除元素完毕
thread1 --- 1
thread2 --- 1
thread2 --- 3
thread2 --- 4
thread2 --- 5
thread1 --- 2
thread1 --- 3
thread1 --- 4
thread1 --- 5
thread3 --- 1
thread3 --- 3
thread3 --- 4
thread3 --- 5

3、CopyOnWriteArrayList的使用场景

通过上面的分析,CopyOnWriteArrayList 有几个缺点:
1)由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc

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

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

4、CopyOnWriteArrayList透露的思想

如上面的分析CopyOnWriteArrayList表达的一些思想:
1)读写分离,读和写分开
2)最终一致性
3)使用另外开辟空间的思路,来解决并发冲突

欢迎小伙伴们留言交流~~
浏览更多文章可关注微信公众号:diggkr

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页