看CopyOnWriteArrayList源代码之后

来源:互联网 发布:淘宝网店图片马云 编辑:程序博客网 时间:2024/05/01 08:49

​       最近写代码的时候傲然F3点进去看了一下源代码,看了之后有几个问题,然后找资料解决了,记录一下。 

       CopyOnWriteArrayList在写的时候复制整个数组,这样操作其实比较耗时,所以这个List适合写少读多的并发场景,此时有很好的性能。 读操作(get(),indexOf(),isEmpty(),contains())不加任何锁,而写操作(set(),add(),remove())通过Arrays.copyOf()操作拷贝当前底层数据结构(array),在其上面做完增删改等操作,再将新的数组置为底层数据结构,同时为了避免并发增删改, CopyOnWriteList在这些写操作上通过一个ReetranLock进行并发控制。

        数据结构:Object数组(含有get和set方法)

        关于读和写:

        链表中单个元素的get操作,直接返回数组中对于的数据。

1
2
3
public E get(int index) {
       return (E)(getArray()[index]);
   }

        链表中元素的设置set操作,先copy一份,然后添加元素,之后再调用setArray来重新赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public E set(int index, E element) {
    //加锁操作
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        //获取原先数据
        Object oldValue = elements[index];
        //比较新旧数据是否相同
        if (oldValue != element) {
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            setArray(newElements);
        else {
            // Not quite a no-op; ensures volatile write semantics这里一般看不懂
            setArray(elements);
        }
        return (E)oldValue;
    finally {
        lock.unlock();
    }
}

1、在set中有个疑问,就是为啥在新老对象相同的时候,还调用了一下setArray函数呢?

http://cs.oswego.edu/pipermail/concurrency-interest/2010-February/006886.html  

从注释来看,是确保volatile的写的语义。至于为啥,看了邮件列表中,应该是为了确保happends-before特性。

http://blog.csdn.net/fg2006/article/details/6397142  

2、看了一下,貌似这个方法调用的比较多System.arraycopy,Arrays.copyof 里面也是调用的这个方法,这个方法是干啥的?

这个在System中是一个native方法,负责数组拷贝,参数简单明了。

1
2
3
4
5
6
7
8
9
/* @param      src      the source array.
 * @param      srcPos   starting position in the source array.
 * @param      dest     the destination array.
 * @param      destPos  starting position in the destination data.
 * @param      length   the number of array elements to be copied.
 */
public static native void arraycopy(Object src,  int  srcPos,
                                    Object dest, int destPos,
                                    int length);

写了个程序对比了一下使用System和自己循环进行数据拷贝的效率问题,还是原生的给力啊。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class ListTest {
    public static void main(String[] args) {
        int size = 1000;
        //每次copy运行的次数
        int runTime = 100000;
        String[] src =  genString(size);
        String[] destSysCopy = new String[size];
        String[] destSelfloop = new String[size];
        /**通过System.arrayCopy来复制*/
        long s1 = System.currentTimeMillis();
        for(int i=0;i<runTime;i++){
            System.arraycopy(src, 0, destSysCopy, 0, src.length);
        }
        System.out.println("arrayCopy:"+(System.currentTimeMillis()-s1)+",size="+destSysCopy.length);
        /**通过循环来完成复制*/
        long s2 = System.currentTimeMillis();
        for(int i=0;i<runTime;i++){
            for(int j=0;j<src.length;j++){
                destSelfloop[j] = src[j];
            }
        }
        System.out.println("selfLoop:"+(System.currentTimeMillis()-s2)+",size="+destSelfloop.length);
    }
    /**初始化算是数组*/
    private static String[] genString(int size){
        String[] a = new String[size];
        for(int i=0;i<size;i++){
            a[i] = "iamzhongyong+"+i;
        }
        return a;
    }
}
1
2
arrayCopy:78,size=1000
selfLoop:453,size=1000

3、关于迭代器

        CopyOnWriteList所实现的迭代器其数据也是底层数组镜像,所以在CopyOnWriteList进行interator,同时并发增删改CopyOnWriteList里的数据实不会抛“ConcurrentModificationException”,当然在迭代器上做remove,add,set也是无效的(抛UnsupportedOperationExcetion),因为迭代器上的数据只是当前List的数据数组的一个拷贝而已。

1
2
3
public Iterator<E> iterator() {
      return new COWIterator<E>(getArray(), 0);
  }
1
2
3
4
5
6
7
8
9
10
public void remove() {
            throw new UnsupportedOperationException();
        }
public void set(E e) {
            throw new UnsupportedOperationException();
        }
   
public void add(E e) {
            throw new UnsupportedOperationException();
        }

 

4、为啥数据对象加了volatile,不加有啥问题?

private volatile transient Object[] array;

        Volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。(http://www.infoq.com/cn/articles/ftf-java-volatile  )

        如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。

        Volatile变量修饰符如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。

        锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility)。

        互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据。

        可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 —— 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发许多严重问题。

        Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。

        Volatile 变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。

        至此能够清楚了,array的值在写的时候,通过synchronized来保证原子性,但是此时还有别的线程可能用到了array这个对象,例如在读的时候,这时候通过volatile能够保证可见性。     

5、CopytOnWriteArrayList中写方法使用RentrantLock来实现同步,为啥不用synchronized呢?

这个可能是历史原因,在jdk6之前,synchronized在多线程情况下吞吐量下降和明显,而Lock的话会好很多,所以并发包中采用了Lock的机制,后来JDK6对于synchronized锁做了优化,两者持平,所以正在synchronized能够满足需求的情况下,优先考虑使用这个,毕竟非常方便吗。

重入锁:指的是同一个线程多次试图获取它所占有的锁,请求会成功。当释放锁的时候,直到重入次数清零,锁才释放完毕。

搞个例子对比一下现在的情况,例子参照:http://www.blogjava.net/killme2008/archive/2007/09/14/145195.html 

Counter接口:

1
2
3
4
public interface Counter {
    public long getValue();
    public void increment();
}

三种情况下的实现:

1
2
3
4
5
6
7
8
9
10
11
public class NoLockCounter implements Counter {
    private long count = 0;
    @Override
    public long getValue() {
        return count;
    }
    @Override
    public void increment() {
        count++;
    }
}
1
2
3
4
5
6
7
8
9
10
11
public class SynchronizeCounter implements Counter {
    private long count = 0;
    @Override
    public long getValue() {
        return count;
    }
    @Override
    public synchronized void increment() {
        count++;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ReentranLockCounter implements Counter {
    private volatile long count = 0;
    private Lock lock = new ReentrantLock();
                                               
    @Override
    public long getValue() {
        return count;
    }
    @Override
    public void increment() {
        lock.lock();
        try{
            count++;
        }finally{
            //切记在finally中释放锁,synchronize的话JVM会自动释放锁
            lock.unlock();
        }
    }
}

单线程情况下的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class LockTest {
    public static void main(String[] args) {
        //单线程情况下基本没啥插件,synchronize略强一点,无锁最好,这个不用说呵呵
        int runTime = 100000000;
        Counter c1 = new SynchronizeCounter();
        Counter c2 = new ReentranLockCounter();
        Counter c3 = new NoLockCounter();
        // 单线程情况下测试
        long s1 = System.currentTimeMillis();
        for(int i=0;i<runTime;i++){
            c1.increment();
        }
        System.out.println("synchronize锁,C1:"+c1.getValue()+",用时:"+(System.currentTimeMillis()-s1));
        long s2 = System.currentTimeMillis();
        for(int i=0;i<runTime;i++){
            c2.increment();
        }
        System.out.println("ReentranLock锁,C2:"+c2.getValue()+",用时:"+(System.currentTimeMillis()-s2));
                                              
        long s3 = System.currentTimeMillis();
        for(int i=0;i<runTime;i++){
            c3.increment();
        }
        System.out.println("无锁,C3:"+c3.getValue()+",用时:"+(System.currentTimeMillis()-s3));
    }
}

多线程情况测试,原生锁和重入锁差别不大,无锁有现成安全问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.util.concurrent.CountDownLatch;
                                             
public class BenchmarkTest {
    private Counter counter;
                                             
    public static void main(String[] args)  throws Exception{
        Counter c1 = new SynchronizeCounter();
        Counter c2 = new ReentranLockCounter();
        Counter c3 = new NoLockCounter();
        //这里和参照例子不同,我用来CountDownLatch来做现成同步
        CountDownLatch latch1 = new CountDownLatch(1000);
        CountDownLatch latch2 = new CountDownLatch(1000);
        CountDownLatch latch3 = new CountDownLatch(1000);
                                             
        //100线程,每个线程加100,增把数据增加到10000
        long s1 = System.currentTimeMillis();
        for(int i=0;i<1000;i++){
            new TestThread(c1,latch1).start();
        }
        latch1.await();
        System.out.println("SynchronizeCounter的结果是:"+c1.getValue()+",用时:"+(System.currentTimeMillis()-s1));
                                             
        long s2 = System.currentTimeMillis();
        for(int i=0;i<1000;i++){
            new TestThread(c2,latch2).start();
        }
        latch2.await();
        System.out.println("ReentranLockCounter结果是:"+c2.getValue()+",用时:"+(System.currentTimeMillis()-s2));
                                             
        long s3 = System.currentTimeMillis();
        for(int i=0;i<1000;i++){
            new TestThread(c3,latch3).start();
        }
        latch3.await();
        System.out.println("无锁的结果是:"+c3.getValue()+",用时:"+(System.currentTimeMillis()-s3));
                                             
    }
}
1
2
3
SynchronizeCounter的结果是:100000,用时:125
ReentranLockCounter的结果是:100000,用时:125
无锁的结果是:99999,用时:109