最近在阅读 《Java 并发编程之美》这本书,感觉学到了很多东西;所以我决定将从事书中学到的思想和一些经典的案例整理成博客的形式与大家分享和交流,如果对大家有帮助别忘了留下点赞和关注捏。
第五部分:Java 并发包中并发 List 源码剖析
5.1)介绍
并发包中的并发 List 只有 CopyOnWriteArrayList
。这是一个线程安全的 ArrayList
,对其进行修改的操作都是在底层的一个复制数组上进行的,也就是使用了写时复制策略。
在类中有一个 array 数组对象用来存放具体的元素:
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
每个 CopyOnWriteArrayList
都有一个对象锁来保证只有一个线程对 array 进行修改操作:
/**
* The lock protecting all mutators. (We have a mild preference
* for builtin monitors over ReentrantLock when either will do.)
*/
final transient Object lock = new Object();
上面的注解的含义是这样的:
保护所有变化的锁。(我们对内置监控器略有偏好,而不是ReentrantLock
,两者都可以。)
这个锁的类型要根据不同的 Java 源码来看,不同的团队会有不同的设计选择,所以如果追源码发现这个锁的实现不同,是因为我们看的源码的版本不同。
比如来看一个其他版本的 Java 源码:
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
这里使用的就是 ReentrantLock
独占锁。
5.2)主要方法源码剖析
5.2.3)初始化方法
首先来看较为简单的无参构造方法,当调用这个构造方法的时候,会给 array 数组赋一个大小为 0 的 Object 数组。
/**
* Creates an empty list.
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}
除了有参构造方法, CopyOnWriteArrayList
类还提供了这些构造方法:
/**
* Creates a list holding a copy of the given array.
*
* @param toCopyIn the array (a copy of this array is used as the
* internal array)
* @throws NullPointerException if the specified array is null
*/
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
// 复制方法
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}
上面展示的是传入对象数组的构造方法,方法中会调用 Arrays.copyOf()
方法来创建一个复制数组,然后将其赋值给 array 变量。
/**
* Creates a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection of initially held elements
* @throws NullPointerException if the specified collection is null
*/
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
// 如果传入数组的类型恰好就是 CopyOnWriteArrayList
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
// 对于其他类型就执行复制的逻辑
elements = c.toArray();
if (c.getClass() != java.util.ArrayList.class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
最后一个就是传入 Collection 实现类的构造方法,首先判断 List
类型是否为 CopyOnWriteArrayList
如果是的话就直接将其中的 array 赋值给这个 CopyOnWriteArrayList
,如果是其他的类型就将其转化为 Array 然后执行 copyOf
操作。
上面的构造方法中,如果发现传入的类是
CopyOnWriteArrayList
就将它底层的数组直接赋值给这个新的对象的 array,这不会导致它们操纵的是一个数组吗?
其实如果不进行任何操作(比如增加元素或者删除元素),这两个对象的底层确实就是同一个数组;但是如果执行了新增或者删除的操作,类中的array
的长度和其中含有元素的长度是相同的,也就是如果新增或者删除元素就会引发数组的更新,此时会将原本的数组复制到一个新的指定长度的数组中,此时两个对象操控的元素就不同了。
写个反射来验证一下:
public class Main {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
CopyOnWriteArrayList<Integer> integers1 = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList<Integer> integers2 = new CopyOnWriteArrayList<>(integers1);
Class<? extends CopyOnWriteArrayList> aClass = integers1.getClass();
Field array = aClass.getDeclaredField("array");
array.setAccessible(true);
Object[] arr1 = (Object[])array.get(integers1);
Object[] arr2 = (Object[])array.get(integers2);
System.out.println(arr1 == arr2);
}
}
在上面的代码中,首先创造了一个 CopyOnWriteArrayList
对象 integers1
,然后将其作为参数传递给新的 CopyOnWriteArrayList
对象,此时使用反射方法拿到它们的 array 属性,通过 ==
检测是否是相同的数组:
最终发现输出的是 true,但是如果在代码中加一个 add()
public class Main {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
CopyOnWriteArrayList<Integer> integers1 = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList<Integer> integers2 = new CopyOnWriteArrayList<>(integers1);
integers1.add(0); // 新增的代码
Class<? extends CopyOnWriteArrayList> aClass = integers1.getClass();
Field array = aClass.getDeclaredField("array");
array.setAccessible(true);
Object[] arr1 = (Object[])array.get(integers1);
Object[] arr2 = (Object[])array.get(integers2);
System.out.println(arr1 == arr2);
}
}
此时再去运行,得到的就是 false 了
5.2.2)添加元素
CopyOnWriteArrayList
类中提供了许多添加元素的方法,有 add(E e)
add(int index, E element)
addifAbsent(E e)
addAllAbsent(Collection<? extends E> c)
等等,但是他们的实现方式都差不多,这里以 add(E e)
方法举例来讲解,通过这个也可以更好的理解上面的解释。
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection#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();
}
}
方法首先会获取独占锁,保证同一个时间只有一个线程去获取这个锁,其他线程会被阻塞直到锁被释放,添加的操作逻辑比较简单,就是将原本的数组内容拷贝到新的长度为原本长度 + 1 的数组中。
5.2.3)获取指定位置的元素
/**
* {@inheritDoc}
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
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];
}
上面展示的三个方法就是 get(int index)
方法调用的几个方法,执行的逻辑就是先获取到 array 数组,然后取出指定位置的元素。
考虑一个问题,这个方法没有枷锁的,如果一个线程在执行的时候,另一个线程调用了 remove 方法,现在获取的元素删除掉导致出现问题吗?
remove 方法和上面的 add 方法的逻辑是相同的,就是将原本的数组复制到一个新的容量的数组中,然后这个数组赋值给 array,但是此时原本的数组并不会销毁,因为它正在被此时调用 get
方法的线程中引用,所以此时获取到的仍然是正确是元素。
5.2.4)修改指定的元素
/**
* Replaces the element at the specified position in this list with the
* specified element.
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
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);
} else {
// 不完全是没有操作; 维护了 volatile 的写入语义
setArray(elements);
}
return oldValue;
} finally {
lock.unlock(); // 释放锁
}
}
修改操作和上面的 add 以及 remove 方法相同,都是首先复制一份数组,然后再新复制的数组上执行替换的操作,即使当发现 oldValue == element 的时候会执行这个操作:setArray(elements); 的操作,这是为了维护 volatile 的语义,因为 array 是一个被 volatile 关键字修饰的属性。
执行这个操作的目的是确保对 volatile 类型数组的写操作具有正确的语义。volatile 关键字用于声明变量,以确保多线程之间的可见性和一致性。
当一个线程修改了 volatile 变量的值时,这个值会立即被写回到主内存中,并且其他线程在读取这个变量时会从主内存中获取最新的值,而不是从自己的线程缓存中获取。
即使知道数组的内容没有变化,也还是要重新去设置 array。
5.2.5)删除元素
/**
* Removes the element at the specified position in this list.
* Shifts any subsequent elements to the left (subtracts one from their
* indices). Returns the element that was removed from the list.
*
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E remove(int index) {
synchronized (lock) { // 获取独占锁
Object[] es = getArray();
int len = es.length;
// 获取指定元素
E oldValue = elementAt(es, index);
// 准备需要使用的变量
int numMoved = len - index - 1;
Object[] newElements;
if (numMoved == 0)
// 如果要删除的是最后一个元素
newElements = Arrays.copyOf(es, len - 1);
else {
// 删除的是其他元素,分两次将内容复制到新的数组
newElements = new Object[len - 1];
System.arraycopy(es, 0, newElements, 0, index);
System.arraycopy(es, index + 1, newElements, index,
numMoved);
}
setArray(newElements);
return oldValue;
}
}
不用猜也可以知道,还是和上面的操作相同,操作仍然是在复制的数组中进行的,上面的代码中首先创建了一个长度为 len - 1 的新数组,然后将 0 到 index - 1 的元素复制到新的数组,再将 index + 1 到末尾的元素复制到新的数组,从而实现了删除的操作;因为除了读取以外的其他操作都需要获取锁,所以这里的删除不会被其他的操作影响到。
5.2.6)弱一致性的迭代器
说到迭代器,大家一定不会陌生,CopyOnWriteArrayList
类实现了 List
接口,所以其迭代器的操作逻辑和 ArrayList
等我们熟悉的集合是完全相同的:
// 演示迭代器的使用
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> integers = new CopyOnWriteArrayList<>();
integers.add(1);
integers.add(12);
integers.add(123);
Iterator<Integer> iterator = integers.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
弱一致性指的是,当返回一个迭代器的时候,其他线程对于 List 的增删改的操作对于这个迭代器是不可见的,下面来看一下具体的实现方式:
// 获取迭代器
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
// 迭代器的初始化方法
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor; // 初始指针
snapshot = elements; // 指向数组的指针
}
当调用 iterator 方法的时候,会返回一个 COWIterator
对象,这个对象的 snapshot 变量保存了当前 list 的内容,cursor 是遍历 list 时数据的下标。
5.3)总结
通过上面源码的阅读,可以发现, CopyOnWriteArrayList
类是通过写时复制的策略来保证 list 的一致性,修改时候的操作不是原子性的,所以在增删改的过程中都使用了独占锁来保证这些操作的原子性。另外,类中还提供了一个弱一致性的迭代器,从而保证获取迭代器之后,其他线程的修改是不可见的,迭代器遍历的数组是一个快照。