源码阅读(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