源码阅读(29):Java中线程安全的List结构——CopyOnWriteArrayList

Posted 说好不能打脸

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了源码阅读(29):Java中线程安全的List结构——CopyOnWriteArrayList相关的知识,希望对你有一定的参考价值。

(接上文《源码阅读(28):Java中线程安全的List结构——CopyOnWriteArrayList(1)》)

4、CopyOnWriteArrayList的主要方法

当完成CopyOnWriteArrayList集合的初始化过程的介绍后,本文再列举几个该集合典型的方法,以便帮助读者理解该集合是如果基于一个内存副本完成写操作的,以及这样做的有点和缺点。

4.1、get(int)方法

get(int)方法是从CopyOnWriteArrayList集合中获取指定索引位置上元素对象的方法,该方法无需保证线程安全性,任务操作者、任何线程、任何时间点都可以通过该方法或类似方法获取CopyOnWriteArrayList集合中的数据,究其根本原因,就是因为该集合的所有写操作都是在一个内存副本中进行,所以任何读性质的操作都不会受影响

public E get(int index) 
  return get(getArray(), index);


/**
 * Gets the array.  Non-private so as to also be accessible
 * from CopyOnWriteArraySet class.
 */
final Object[] getArray() 
    return array;


@SuppressWarnings("unchecked")
private E get(Object[] a, int index) 
  return (E) a[index];

代码很简单,以至于无非进行任何说明。这种数据读取方式因为不需要考虑任何锁机制,并且数组可以支持随机位置上的读操作,所以其时间复杂度任何时候都为O(1)。

4.2、add(E)方法

使用add方法,向CopyOnWriteArrayList集合的最后一个数组索引位添加一个新的元素(引用),添加的元素可以为null。源代码片段如下所示:

public boolean add(E e) 
  final ReentrantLock lock = this.lock;
  // 通过lock所对应获取操作以下代码的操作权
  lock.lock();
  try 
    // 获取到当前集合使用的数组对象
    Object[] elements = getArray();
    // 获取当前集合的元素大小
    int len = elements.length;
    // 使用Arrays.copy方法创建一个内存副本newElements数组
    // 注意,副本数组的容量比当前CopyOnWriteArrayList集合的容量大1
    Object[] newElements = Arrays.copyOf(elements, len + 1);
    // 在newElements数组的最后一个索引位新增这个元素(引用)
    newElements[len] = e;
    // 设置完成后,最后将当前使用数组替换成副本,是副本数组成为CopyOnWriteArrayList集合的内部数组
    setArray(newElements);
    return true;
   finally 
    // 最后释放操作权
    lock.unlock();
  

这里需要注意一个细节:在add方法所有处理逻辑开始前,先进行CopyOnWriteArrayList集合的操作权获取,它并不影响CopyOnWriteArrayList集合的读操作,因为通过上一小节中介绍get方法的源代码内容可知,CopyOnWriteArrayList集合的读操作完全无视锁权限,也不会有多线程下的数据操作问题。之所以类似add方法这样的CopyOnWriteArrayList容器写操作方法需要获取操作权,主要是为了防止其它线程可能对CopyOnWriteArrayList集合同时进行的写操作造成数据错误

从以上add方法的详细描述我们可以知道,该集合通过Arrays.copyOf方法(其内部是System.arraycopy方法)创建一个新的内存区域,存放数组副本,并在副本上进行写操作,最后将CopyOnWriteArrayList集合中的数组引用为副本数组。

4.3、set(int , E)方法

set方法用于替换CopyOnWriteArrayList集合指定数组索引位上的元素。该方法的操作过程和add方法类似,源代码如下所示:

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;
      setArray(newElements);
    
    // 如果原始对象和将要重新设定的对象相等(依据内存地址)
    // 从理论上讲,无需对CopyOnWriteArrayList集合的当前数组重新进行设定,
    // 但这里还是重新设定了一次
    else 
      // Not quite a no-op; ensures volatile write semantics
      setArray(elements);
    
    // 处理完成后,该索引为上原始的数据对象将会被返回
    return oldValue;
   finally 
    lock.unlock();
  

请注意源代码中的一句注释:“Not quite a no-op; ensures volatile write semantics”,当指定数组索引位上的原始数据对象和将要新替换的数据对象“相等”时,从理论上讲实际上就不需要创建副本进行写操作,也不再需要通过setArray方法进行数组替换操作了。

但从以上源代码中我们可以看到当以上场景出现时,源代码仍然调用了setArray方法进行数组的设置操作,为什么会这样呢?这主要是为了保证外部调用者的非volatile变量遵循happen−before原则。该原则涉及到JMM(java内存模型)和指令重排的知识点,有兴趣的读者可自行查询资料。

5、Collections.synchronizedList的补充作用

5.1、CopyOnWriteArrayList工作机制的优缺点

从以上关于CopyOnWriteArrayList集合的工作介绍中,我们可以大致归纳出CopyOnWriteArrayList集合的特点和它的一些优缺点:

  • 该集合适合应用在多线程并行场景下,如果读者使用集合的场景中不涉及多线程操作,则不建议使用该集合。甚至不建议使用java.util.concurrent包下的任何集合类——使用java.util包中的基本java集合框架即可。

  • 该集合在多线程并发操作的场景下,主要的关注点集中在于如何保证集合的线程安全性,和集合的数据读操作性能。为此,该集合以显著牺牲自身的写操作性能和内存空间的方式,来换取读操作性能不受影响。这个特征很好理解,每次进行读操作前都要创建一个内存副本,这种操作一定会对内存空间造成浪费,且内存复制操作一定会造成多余的性能消耗。

  • 所以这种集合适用于多线程并发操作场景下,那些多线程读操作远远大于写操作次数,且集合中存储的数据规模不大的场景。

5.2、Collections.synchronizedList对CopyOnWriteArrayList的补充

那么Java原生类、工具包中,有没有提供一些适合在多线程并发操作场景下使用的,其读操作性能和写操作性能保持一定平衡性的,虽然整体性能不是最好但依然保证线程安全的,最后又是List性质的集合呢?答案是:有的。

java.util.Collections是Java为开发人员提供的一个和集合操作相关的工具包(JDK1.2便开始提供,各版本又做了不同程度的代码调整),其中提供了一组方法,可以将java.util包下的那些线程不安全的集合转变为线程安全的集合。实际上就是使用java object monitor机制,将集合方法进行了封装。请看如下示例:

// ......
// 通过Collections提供的synchronizedList方法
// 将线程不安全的ArrayList封装为线程安全的List集合
List<String> syncList = java.util.Collections.synchronizedList(new ArrayList<>());
// 对于使用者来说,集合的增删改查功能不受影响
syncList.add("a");
syncList.add("b");
syncList.add("c");
syncList.add("d");
syncList.add("e");
// ......

// 通过Collections提供的synchronizedList方法
// 将线程不安全的TreeSet集合封装为线程安全的Set集合
Set<String> syncSet = java.util.Collections.synchronizedSortedSet(new TreeSet<>());
syncSet.add("a");
syncSet.add("b");
syncSet.add("c");
syncSet.add("d");
syncSet.add("e");
// ......

但是,使用经过这个工具封装的集合需要特别注意一点,就是原始集合的迭代器(iterator)、可拆分的迭代器(spliterator)、处理流(stream)、并行流(parallelStream)的运行都不受线程安全的封装保护,如果用户需要这样的集合使用方式,则必须自行控制线程安全。

另外,由于java.util.Collections.synchronizedXXXX这样的线程安全集合封装方式,其内部使用的是java object monitor这种锁机制,所以它也不适合在并发量非常高的场景中使用。最后我们基于java.util.Collections.synchronizedList方法,简述一下其内部的工作原理:

public class Collections 
  // Suppresses default constructor, ensuring non-instantiability.
  private Collections() 
  
  
  // ......
  // 其中包括了Collection接口中各方法的实现
  static class SynchronizedCollection<E> implements Collection<E>, Serializable 
    // Backing Collection
    // 被封装的真实集合,由该属性记录(引用)
    final Collection<E> c;
    // Object on which to synchronize
    // 整个集合线程安全性封装的机制中,使用该对象管理Object Monitor锁机制
    final Object mutex;
    // ......
    // 以下诸如size、add这样的集合操作方法,全部基于mutex对象,基于Objct Monitor进行封装
    public int size() 
      synchronized (mutex) return c.size();
    
    public boolean add(E e) 
      synchronized (mutex) return c.add(e);
    
    // ......
    // 以下这些方法没有进行线程安全封装,需要使用者手动控制
    // Must be manually synched by user!
    public Iterator<E> iterator() 
      return c.iterator(); 
    
    // Must be manually synched by user!
    @Override
    public Spliterator<E> spliterator() 
      return c.spliterator(); 
    
    // Must be manually synched by user!
    @Override
    public Stream<E> stream() 
        return c.stream(); 
    
    // Must be manually synched by user!
    @Override
    public Stream<E> parallelStream() 
        return c.parallelStream(); 
    
  
  
  // ......
  
  // 该类继承于SynchronizedCollection
  static class SynchronizedList<E> extends SynchronizedCollection<E> implements List<E> 
    // 被封装的真实List集合,由该属性记录(引用)
    final List<E> list;
	// 并且通过构造函数的重写,将父类中c属性赋值为当前的list属性
    SynchronizedList(List<E> list) 
      super(list);
      this.list = list;
    
    SynchronizedList(List<E> list, Object mutex) 
      super(list, mutex);
      this.list = list;
    
    // ......
    // 诸如以下由list接口定义的操作方法,也被重新封装
    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);
    
  

  // ......
  
  // 该类继承于SynchronizedList,当封装的List性质的集合支持RandomAccess随机访问
  // 就使用该类进行线程安全性封装
  static class SynchronizedRandomAccessList<E> extends SynchronizedList<E> implements RandomAccess 
    public List<E> subList(int fromIndex, int toIndex) 
      synchronized (mutex) 
        return new SynchronizedRandomAccessList<>(list.subList(fromIndex, toIndex), mutex);
      
    
  

以上源代码展示了SynchronizedRandomAccessList、SynchronizedList和SynchronizedCollection这三个工具类的继承关系,以及它们三者是如何配合完成集合线程安全性封装控制的,如下图所示:

现在我们来看一下当调用java.util.Collections.synchronizedList方法时发生了什么事情:

public static <T> List<T> synchronizedList(List<T> list) 
  // 如果当前List集合支持RandomAccess随机访问,则使用SynchronizedRandomAccessList对集合进行线程安全性封装
  // 否则使用SynchronizedList对集合进行线程安全性封装
  return (list instanceof RandomAccess ? new SynchronizedRandomAccessList<>(list) : new SynchronizedList<>(list));

以上是关于源码阅读(29):Java中线程安全的List结构——CopyOnWriteArrayList的主要内容,如果未能解决你的问题,请参考以下文章

源码阅读(28):Java中线程安全的List结构——CopyOnWriteArrayList

源码阅读(32):Java中线程安全的QueueDeque结构——ArrayBlockingQueue

源码阅读(32):Java中线程安全的QueueDeque结构——ArrayBlockingQueue

源码阅读(35):Java中线程安全的QueueDeque结构——LinkedBlockingQueue

源码阅读(35):Java中线程安全的QueueDeque结构——LinkedBlockingQueue

源码阅读(39):Java中线程安全的QueueDeque结构——LinkedTransferQueue