线程安全集合之CopyOnWriteArrayList详解

本篇文章主要介绍线程安全集合之CopyOnWriteArrayList

前言:通过前面对Java集合的学习我们可以知道以下知识点:
Map接口中的HashMap是线程不安全的,而HashTable、ConcurrentHash是线程安全的。同样的,对于Collection接口中,我们也知道ArrayList是线程不安全的,那么除了Vector是线程安全的,是否还有其他集合也是线程安全的呢?这里我们就要讲到List集合中的线程安全的集合CopyOnWriteArrayList

一.CopyOnWriteArrayList介绍

1.简介
从字面上理解就是写时复制,这是什么意思呢?
它的意思是指:假设有多个线程同时对集合进行操作,那么,这些线程都可以共享这些资源,如果有某个或某些线程想要修改集合中的内容时,这时,系统会复制一个新副本(与原集合相同)提供给需要修改的线程进行修改,而其他的线程依旧是对原集合进行操作。假设多个线程都是对原集合进行读取而没有修改,那么此时都是对原集合的操作。

概括一下就是:
1.CopyOnWriteArrayList是线程安全数组。
2.如果多个线程对集合进行操作且某个线程需要对集合进行修改时,那么它的底层是通过复制数组的方式来实现,即,给需要对集合进行修改的线程提供原数组的复制后对复制的数组进行修改。
3.假设多个线程都是对集合进行读取操作并不修改集合时,多个线程都是对原集合的操作,即共享同一个资源

2.源码分析
既然需要进一步了解CopyOnWriteArrayList,我们就需要对它的源码进行剖析。
结构:

final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
final Object[] getArray() {
    return array;
 }
final void setArray(Object[] a) {
    array = a;
 }
 public CopyOnWriteArrayList() {
     setArray(new Object[0]);
}

分析:
final transient ReentrantLock lock = new ReentrantLock();
此部分是定义可重入锁。

关于 ReentrantLock,这部分需要先了解的知识点有以下几点:
1.ReentrantLock是可重入锁,当一个线程获取锁时,还可以接着重复获取多次
2.ReentrantLock在重入时要确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获取锁
3.ReentrantLock默认是实现非公平锁,非公平锁性能更好,但也可以实现公平锁,在创建ReentrantLock时通过传入参数true来实现公平锁,而传入false或者不传参数则实现非公平锁

private transient volatile Object[] array;
此部分是CopyOnWriteArrayList的底层由数组实现,volatile修饰

volatile修饰符的作用:
1.用来修饰被不同线程访问修改的变量
2.某个线程对volatile修饰的变量进行修改,其他线程都是可见的,即获取到的volatile变量的值都是最新的。即,当一个共享变量被volatile修饰时,它会保证修改的值立即更新到内存中。
3.重要:对于一个volatile变量的写操作先行发生于后面对这个变量的读操作
4.volatile可以保证内存的可见性,但不保证变量的原子性

transient:
使得属性不需要被序列化,例如:用户的密码等敏感信息,为了安全起见,不需要其被序列化。
序列化:
为了保存在内存中的各种状态,并且可以把保存对象的状态读出来

综上:CopyOnWriteArrayList是由数组实现,且创建的数组是非序列化、可以被不同线程访问和修改的。

final Object[] getArray() {
return array;
}
此部分是定义方法得到该数组

final void setArray(Object[] a) {
array = a;
}
此部分是定义方法设置该数组,指向该数组

setArray(new Object[0]);
此部分是初始化该数组

二.CopyOnWriteArrayList常用方法源码分析

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

通过add源码我们可以分析为以下几个步骤:
1.设置可重入锁
2.获取当前数组的长度和元素
3.复制新数组,且长度增加一
4.拷贝原数组元素到复制后的新数组
5.将指向原数组的引用指向新数组
6.解除可重入锁

其实add()方法就是一个写时复制的一个很好地例子:需要对数组进行修改add操作时,并不是对原数组直接修改,而是对复制了原数组的新数组进行修改,这样,每个线程都是对各自的新数组进行修改,保证了线程安全。

2.get()方法

public E get(int index) {
    return get(getArray(), index);//返回 array数组中的index位置的元素
}
final Object[] getArray(){	
	return array;//返回数组
}

直接获取数组中对应index位置的元素。

3.set()方法

public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
    	//获取原数组的值
        Object[] elements = getArray();
        E oldValue = get(elements, index);
        //判断原数组的值和新的值是否相等
        if (oldValue != element) {
            //复制一个新数组,用新值替换旧值
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len);
            newElements[index] = element;
            //将array的引用指向新数组
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
        }
        //若新值和旧值一致,则不修改元素
        return oldValue;
    } finally {
   	 //解锁
        lock.unlock();
    }
}

通过以上的set()的源码我们可以知道,在修改index位置的元素时操作步骤如下:
1.设置可重入锁
2.得到原数组的index位置的元素
3.判断此时需要修改新的元素和原来的旧元素是否一致
4.若不一致,说明需要对index位置的元素进行修改替换
5.复制一个新数组
6.将新数组的index位置的元素设置为需要修改的新元素
7.将原数组的引用指向新数组
8.若需要修改的新元素和原数组的index位置的元素一致,则不修改,直接反回旧值
9.finally块中解锁

4.remove()方法

//删除元素的真正实现
    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        //获得可重入锁
        lock.lock();
        try {
            // 获取原始volatile数组中的数据和数据长度。
            Object[] elements = getArray();
            int len = elements.length;
            // 获取elements数组中的第index个数据。
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            // 如果被删除的是最后一个元素,则直接通过Arrays.copyOf()进行处理,而不需要新建数组。
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                // 否则,新建数组,然后将”volatile数组中被删除元素之外的其它元素“拷贝到新数组中;
                // 最后,将”volatile数组“引用指向newElements数组,这样旧数组就被GC回收了。
                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();
        }
    }

通过remove()源码可知,删除index位置的元素的步骤为:
1.获取可重入锁
2.获取原数组的长度和元素
3.获取原数组index位置的元素
4.如果被删除的是最后一个元素,则直接通过Arrays.copyOf()进行处理,而不需要新建数组,只复制原数组len-1的长度,去除最后一个元素
5.如果删除的不是最后一个元素,复制新数组,大小为原数组的长度-1
6.将原数组被删除的元素之外的未被删除的元素全部拷贝到新数组
7.将原数组的引用指向新数组
8.释放可重入锁

遍历iterator()方法

public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
}
//定义成员属性
private final Object[] snapshot;
private int cursor;    // 游标
//构造方法
private COWIterator(Object[] elements, int initialCursor) {
    cursor = initialCursor;
    snapshot = elements;
}
//获取下一个元素
public E next() {
    if (! hasNext())
        throw new NoSuchElementException();
    return (E) snapshot[cursor++];
}
//获取上一个元素
public E previous() {
    if (! hasPrevious())
        throw new NoSuchElementException();
    return (E) snapshot[--cursor];
}
public int nextIndex() {
    return cursor;//返回下一个游标位置
}
public int previousIndex() {
    return cursor-1;//返回上一个游标位置
}

通过iterator()方法的源码可以知道:

CopyOnWriteArrayList在使用迭代器的时候是使用的传入的且复制给快照数组的新数组,如果有多个线程进行修改数组,那么,这些线程修改的都是各自复制的数组,各个线程之间无影响。

以上就是CopyOnWriteArrayList一些常用的方法的源码的分析。

三.CopyOnWriteArrayList的缺点

1.内存占用:
如果经常使用CopyOnWriteArrayList进行元素的修改,就需要执行add()、remove()、set()等方法,这些方法都可能对数组进行复制操作,非常消耗内存。

2.复制数组的安全隐患
我们知道,CopyOnWriteArrayList在多个线程的操作下,如果某些线程对数组进行修改,那么,这些线程都会复制各自的新数组并对其操作,这时,使用迭代遍历时不会发生异常,但实际可能发生安全隐患。

3.数据一致性
CopyOnWriteArrayList只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。(当执行add或remove操作没完成时,get获取的仍然是旧数组的元素)。

以上就是对CopyOnWriteArrayList的部分详解,希望通过学习此篇文章,大家在线程安全的集合问题上能有所了解。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值