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

Posted 说好不能打脸

tags:

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

1、概述

从本文开始我们将介绍多线程(高并发)场景下的多种数据结构,这些数据结构基本来源于java.util.concurrent工具包。通过后续多篇文章的讲解我们将会发现,高并发场景下的数据结构,其关注的最优先问题往往不是数据结构的整体性能,而是数据正确性和特定使用场景下的高性能。所以:

如果你的业务代码中,不存在线程同时操作数据结构的场景,那就不需要使用这些线程安全的数据结构。

如下代码场景中,就是不推荐使用线程安全的数据结构的一个典型场景:

// ......
public void resolve() 
  // ......
  Set<String> mySet = new ConcurrentSkipListSet<>();
  mySet.add("a");
  mySet.add("b");
  mySet.add("c");
  mySet.add("d");
  // ......

// ......

ConcurrentSkipListSet集合是一种存在于java.util.concurrent工具包中,基于跳跃表结构的线程安全的Set性质集合。以上代码中,变量mySet引用的对象是方法(方法名resolve)的局部变量,根据JVM的内存结构我们可以知道,这种局部变量只要没有被全局变量引用,就不会存在线程安全性问题。所以以上代码中建议直接使用HashSet集合或者TreeSet这样的数据结构,而不是ConcurrentSkipListSet集合。

2、CopyOnWriteArrayList的基本工作原理

Copy On Write思想从字面上的理解就是写时复制。当进行指定数据的写入时,为了不影响其它线程同时在进行的集合数据读操作,使用的策略是:在进行写操作前首先复制一个副本出来,在副本上进行写操作,这样就不会影响当前数据的读操作。当副本完成写操作后,最后再将当前数据替换成副本。

很多软件在设计上都采用了Copy On Write思想,最典型,最被技术人员熟知的就是Redis中对Copy On Write思想的应用。Redis为了保证其读性能,在周期性进行的RDB(持久化)操作时就使用了Copy On Write思想。由于RDB的操作时间主要取决于磁盘I/O性能,所以内存中需要持久化的数据过大,就会产生较长的操作时间从而影响Redis性能——这很好理解,如果在进行持久化操作的同时,被写入的数据一直有在变化就会导致数据不一致。

那么改进办法就是,在进行持久化操作前,先做一个当前数据的副本,并根据副本内容进行持久化操作。这样当前数据的状态就被固话下来,并且不影响对原始数据的任何操作,如下图所示:

Copy On Write思想在Java中的具体实现就是CopyOnWriteArrayList集合,该集合在进行写操作时都会创建一个内存副本,并在副本中进行相关操作,最后使用副本内存空间替换真实的内存空间。但是创建副本空间是有性能消耗的,特别是当CopyOnWriteArrayList集合中的数据量较大时。所以CopyOnWriteArrayList集合适合读操作远远大于写操作,且使用时需要保证集合读性能的多线程场景。下面本文开始详细介绍这个集合的内部结构和工作原理:

2.1、CopyOnWriteArrayList的内部结构和工作原理

首先我们来看一下CopyOnWriteArrayList集合的主要属性:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable 
  // ......
  /** The lock protecting all mutators */
  // 这个锁不是为了保证读写互斥,准确来说,该集合的读操作并不会加锁
  // 这个锁的主要作用是保证多线程在同时抢占集合的写操作权限时的数据安全性
  final transient ReentrantLock lock = new ReentrantLock();

  /** The array, accessed only via getArray/setArray. */
  // 该属性用来真实保存集合中的数据
  private transient volatile Object[] array;
  // ......

如以上代码所示,CopyOnWriteArrayList集合类中只有两个关键属性。lock属性是一个“可重入锁”对象,CopyOnWriteArrayList集合在多线程操作场景下,主要就是由它控制线程操作权限,保证集合中元素在多线程写操作场景下的数据正确性。

另外该类除了直接实现java.util.List接口外,还实现了java.util.RandomAccess接口,后者在前文中介绍过:这是一种标识接口,它代表了实现者对于随机索引位置上的数据读取性能不受集合存储的数据规模影响——既是读取数据的时间复杂度始终为O(1)。另外关于java.util.RandomAccess接口的介绍,可以参考文章:《源码阅读(1):Java中主要的List结构——概述

2.2、不支持的使用场景

由于CopyOnWriteArrayList集合在进行数据写操作时,是依靠一个副本进行的,所以一些必须在原始数据上操作的方式就不能支持了。最显而易见的是:在迭代器上进行的元素更改操作(remove、set和add)不受支持。如下所示:

public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable 
  // ......  
  private transient volatile Object[] array;
  // ......
  public Iterator<E> iterator() 
    return new COWIterator<E>(getArray(), 0);
  
  
  // 这是存在于CopyOnWriteArrayList内部的迭代器
  private static class COWSubListIterator<E> implements ListIterator<E> 
    // 类似以下这些“读”性质的方法不受影响
    public boolean hasNext() 
      return nextIndex() < size;
    
    public E next() 
      if (hasNext())
        return it.next();
      else
        throw new NoSuchElementException();
    
    public E previous() 
      if (hasPrevious())
        return it.previous();
      else
        throw new NoSuchElementException();
    
    
    // 但类似如下的写操作方法,由于不能直接在非副本区域进行写操作,所以都不支持
    // 不允许通过迭代器直接进行移除操作
    public void remove() 
      throw new UnsupportedOperationException();
    
    // 不允许通过迭代器,直接修改数据
    public void set(E e) 
      throw new UnsupportedOperationException();
    
    // 不允许通过迭代器,直接添加数据
    public void add(E e) 
      throw new UnsupportedOperationException();
    
  

3、CopyOnWriteArrayList的主要构造函数

CopyOnWriteArrayList一共有三个构造函数,如下所示:

public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable 
  // ......
  // 该属性的意义已经介绍过,这里不再赘述
  private transient volatile Object[] array;
  
  // 默认的构造函数,当前array数组的集合容量为1,且这个唯一的索引位上的数据为null
  public CopyOnWriteArrayList() 
    setArray(new Object[0]);
  
  
  // 该构造函数可以接受一个外部集合,进行实例化
  // 实例化时第三方集合中的元素将会被复制(引用)到新的CopyOnWriteArrayList集合实例中
  public CopyOnWriteArrayList(Collection<? extends E> c) 
    Object[] elements;
    // 如果外部参照的集合也是一个CopyOnWriteArrayList集合,就可以直接copy(引用)参照集合的array数组
    if (c.getClass() == CopyOnWriteArrayList.class)
      elements = ((CopyOnWriteArrayList<?>)c).getArray();
    // 否则将c集合中的元素按照一维数组返回后,再通过Arrays.copyOf方法(按场景)
    // 形成CopyOnWriteArrayList集合中基于array数组存储的元素(引用)
    else 
      elements = c.toArray();
      // c.toArray might (incorrectly) not return Object[] (see 6260652)
      if (elements.getClass() != Object[].class)
        elements = Arrays.copyOf(elements, elements.length, Object[].class);
    
    setArray(elements);
  
  
  // 该构造函数从外部接受一个数组,并通过Arrays.copyOf方法
  // 形成CopyOnWriteArrayList集合中基于array数组存储的元素(引用)
  public CopyOnWriteArrayList(E[] toCopyIn) 
    setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
  

  final void setArray(Object[] a) 
    array = a;
  

请注意以上代码中一句源代码注释:

// c.toArray might (incorrectly) not return Object[] (see 6260652)

这是从JDK1.5版本开始出现的一个java bug,该bug已经在JDK9(b73)得到了修复。该bug的详细情况可以查看如下地址:
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6260652

简单来说这个bug阐述了c.toArray()方法返回的结果不一定是一个真实类型为Object的一维数组,请看如下代码:

// ......
List<String> myList = Arrays.asList(new String[] "1","2","3");
Object[] objects = myList.toArray();
// 通过显示出来的结果我们知道了,objects数组的真实类型是String
System.out.println(objects.getClass().getSimpleName());

List<String> myList2 = new ArrayList<>();
myList2.add("1");
myList2.add("2");
Object[] otherObjects = myList2.toArray();
// 通过显示出来的结果,我们知道了otherObjects数组的真实类型是Object
System.out.println(otherObjects.getClass().getSimpleName());
// ......

产生以上差异的原因,主要是因为Arrays.asList内置了一个List接口的实现类,记为Arrays.ArrayList类,后者使用泛型标识入参类型,如下所示:

// Arrays类是java提供出来的经常被使用的一个数组工具类
public class Arrays 
  // ......
  @SafeVarargs
  @SuppressWarnings("varargs")
  public static <T> List<T> asList(T... a) 
    return new ArrayList<>(a);
  
  // ......
  private static class ArrayList<E> extends AbstractList<E> implements RandomAccess, java.io.Serializable 
    // 使用泛型类数组而不是明确的Object数组标识入参
    private final E[] a;
    ArrayList(E[] array) 
      a = Objects.requireNonNull(array);
    
  
  // ......

这样一来,我们使用Arrays.asList()方法得到的List数组,其内部数组真实的类型就不再是Object而是String。那么当我们在类似如下的场景向添加新的元素时,就会抛出异常:

// 这时cowList内部的Object[]数组的真实类型就是String,而不是Object
// myList既是以上代码片段中,通过Arrays.asList()方法得到的List
CopyOnWriteArrayList<?  super CharSequence> cowList = new CopyOnWriteArrayList<>(myList);
// 那么我们进行如下添加时,系统就会抛出异常
// java.lang.ArrayStoreException,原因是cowList内部数组的真实类型是String
CharSequence item = new StringBuffer("3");
cowList.add(item);

为了解决这个数组类型错误的问题,在CopyOnWriteArrayList的源代码中,增加了一个如下的判定,保证当外部集合通过toArray()方法得到的数组集合类型不为Object[]时,通过Arrays.copyOf()方法将前者复制(引用)成一个Object[]类型的数组:

public CopyOnWriteArrayList(Collection<? extends E> c) 
  // ......
  else 
    elements = c.toArray();
    // c.toArray might (incorrectly) not return Object[] (see 6260652)
    if (elements.getClass() != Object[].class)
      // 如果判定elements数组的真实类型不是Object,则通过Arrays.copyOf方法进行元素复制,
      // 保证elements数组的真实类型是一个Object
      elements = Arrays.copyOf(elements, elements.length, Object[].class);
  
  setArray(elements);

========
(接下文《源码阅读(29):Java中线程安全的List结构——CopyOnWriteArrayList(2)》)

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

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

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

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

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

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

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