第 5 章 Java 并发包中并发 List 源码剖析

发布于:2024-11-28 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

5.1 介绍

5.2 主要方法源码解析

初始化 

5.2.2 添加元素 

5.2.3 获取指定位置元素 

5.2.4 修改指定元素 

5.2.5 删除元素

5.2.6 弱一致性的迭代器 


5.1 介绍

  1. 并发包中的并发 List 只有 CopyOnWriteArrayList。CopyOnWriteArrayList 是一个线程安全的 ArrayList,对其进行的修改操作都是在底层的一个复制的数组(快照)上进行的,也就是使用了写时复制策略,用来保证 list 的一致性,
  2. 但是获取—修改—写 入三步操作并不是原子性的,所以在增删改的过程中都使用了独占锁ReentrantLock,来保证在某个时间只有一个线程能对 list 数组进行修改。
  3. 但是在“获取指定位置元素”时没有加锁同步,所以 CopyOnWriteArrayList 提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对 list 的修改是不可见的。

5.2 主要方法源码解析

初始化 

//无参构造函数,在内部创建了一个大小为 0 的 Object 数组作为
//array 的初始值。

 public CopyOnWriteArrayList() {

 setArray(new Object[0]);

 }

//有参构造函数。
//创建一个list,其内部元素是入参toCopyIn的副本
 public CopyOnWriteArrayList(E[] toCopyIn) {

 setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));

 }

 //入参为集合,将集合里面的元素复制到本list
 public CopyOnWriteArrayList(Collection<? extends E> c) {
     Object[] elements;
     if (c.getClass() == CopyOnWriteArrayList.class)
     elements = ((CopyOnWriteArrayList<?>)c).getArray();
     else {
             elements = c.toArray();
    
     if (elements.getClass() != Object[].class)
             elements = Arrays.copyOf(elements, elements.length, Object[].class);
     }
 setArray(elements);
 }

5.2.2 添加元素 

CopyOnWriteArrayList 中用来添加元素的函数有 add(E e) , add(int index, E element),
addIfAbsent(E e) addAllAbsent(Collection<? extends E> c) 等。
public boolean add(E e) {
	//获取独占锁(1)
	final ReentrantLock lock = this.lock;
	lock.lock();
	try {
		//(2)获取array
		Object[] elements = getArray();

		//(3)复制array到新数组,添加元素到新数组
		int len = elements.length;
//从这里可以知道新数组的大小是原来数组大小增加 1,所以 CopyOnWriteArrayList 是无界 list)
		Object[] newElements = Arrays.copyOf(elements, len + 1);
//把新增的元素E e添加到新数组。
		newElements[len] = e;

		//(4)使用新数组替换添加前的数组
		setArray(newElements);
		return true;

	} finally {

		//(5)释放独占锁
		lock.unlock();
	}
} 
由于加了锁,所以整个 add 过程是个原子性操作。需要注意的是,在添加元素时,首先 复制了一个快照 ,然 后在快照上进行添加,而不是直接在原来数组上进行。

5.2.3 获取指定位置元素 

使用 E get(int index) 获取下标为 index 的元素,如果元素不存在则抛出IndexOutOfBoundsException 异常。
public E get(int index) {
//(1)先获取array 数组
return get(getArray(), index);
}

final Object[] getArray() {
return array;
}

//(2)通过下标访问指定位置的元素
private E get(Object[] a, int index) {
return (E) a[index];
}

这个过程没有加锁同步,会出现一些问题,以下具体分析:

由于执行步骤 A(获取array 数组 ) 和步骤 B (通过下标访问指定位置的元素)没有加锁,这就可能导致在线程 x 执行完步骤 A 后执行步骤 B 前,另外一个线程 y 进行了 remove 操作,线程y结束后, 这时线程 x 开始执行步骤 B ,步骤 B 操作的数组是线程 y 删除元素之前的数组,
所以,虽然线程 y 已经删除了 index 处的元素,但是线程 x 的步骤 B 还是会返回
index 处的元素,这其实就是 写时复制策略 产生的 弱一致性问题

5.2.4 修改指定元素 

使用 E setint index,E element 修改 list 中指定元素的值,如果指定位置的元素不存在则抛出 IndexOutOfBoundsException 异常
public E set(int index, E element) {
//获取了独占锁,从而阻止其他线程对 array 数组进行修改
	final ReentrantLock lock = this.lock;
	lock.lock();
	try {
		Object[] elements = getArray();
//调用 get 方法获取指定位置的元素
		E oldValue = get(elements, index);

//如果指定位置的元素值与新值不一致则创建新数组并复制元素,
//然后在新数组上修改指定位置的元素值并设置新数组到 array。
		if (oldValue != element) {
			int len = elements.length;
			Object[] newElements = Arrays.copyOf(elements, len);
			newElements[index] = element;
			setArray(newElements);
		} else {
			//为了保证 volatile 语义,还是需要重新设置 array,
			setArray(elements);
		}
		return oldValue;
	} finally {
		lock.unlock();
	}
}

5.2.5 删除元素

删除 list 里面指定的元素 , 可以使用 E remove(int index) boolean remove(Object o)
boolean remove(Object o, Object[] snapshot, int index) 等方法
public E remove(int index) {

 //获取独占锁
 final ReentrantLock lock = this.lock;
 lock.lock();

 try {

 //获取数组
 Object[] elements = getArray();
 int len = elements.length;

 //获取指定元素
 E oldValue = get(elements, index);
 int numMoved = len - index - 1;

 //如果要删除的是最后一个元素
 if (numMoved == 0)
 setArray(Arrays.copyOf(elements, len - 1));

 else {

 //分两次复制删除后剩余的元素到新数组
 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();
 }
 }

首先获取独占锁以保证删除数据期间其他线程不能对 array 进行修改,然后获取数组中要被删除的元素,并把剩余的元素复制到新数组之后使用新数组替换原来的数组,最后在返回前释放锁。

5.2.6 弱一致性的迭代器 

迭代器使用:遍历列表元素。

{
 CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();
 arrayList.add("hello");
 arrayList.add("alibaba");
 
 Iterator<String> itr = arrayList.iterator();
 while(itr.hasNext()){
 System.out.println(itr.next());
 }
}
弱一致性 是指返回迭代器后其他线程对 list 的增删改迭代器是不可见的。
也就是说,如果在线程1中我最先获取了迭代器,线程2,3对数组元素进行增删改,然后执行完线程2,3后,线程1用获得的迭代器遍历数组,遍历的结果和初始数组一样,迭代器不会管(不可见)线程2,3对数组的更新后的新数组。
public Iterator<E> iterator() {
 return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
 //array的快照版本
 private final Object[] snapshot;
 
 //数组下标
 private int cursor;
 
 //构造函数
 private COWIterator(Object[] elements, int initialCursor) {
 cursor = initialCursor;
 snapshot = elements;
 }
 //是否遍历结束
 public boolean hasNext() {
 return cursor < snapshot.length;
 }
 
 //获取元素
 public E next() {
 if (! hasNext())
 throw new NoSuchElementException();
 return (E) snapshot[cursor++];
 }
为什么说 snapshot list 的快照呢?
明明是指针传递的引用,而不是副本。那是因为如果在该线程使用返回的迭代器遍历元素的过程中,其他线程没有对 list 进行增删改,那么 snapshot 本身就是 list array ,因为它们是引用关系。但是如果在遍历期间其他线程对该 list进行了增删改,那么 snapshot 就是快照了,因为增删改后 list 里面的数组被新数组替换了, 这时候老数组被 snapshot 引用。这也说明获取迭代器后,使用该迭代器元素时,其他线程对该 list 进行的增删改不可见,因为它们操作的是两个不同的数组,这就是弱一致性。所以说, 迭代器遍历的数组是一个快照
下面通过一个例子来演示多线程下迭代器的弱一致性的效果。可以自己运行查看结果
public class copylist
{
private static volatile CopyOnWriteArrayList<String> arrayList = new
CopyOnWriteArrayList<>();
public static void main( String[] args ) throws InterruptedException
{
arrayList.add("hello");
arrayList.add("alibaba");
arrayList.add("welcome");
arrayList.add("to");
arrayList.add("hangzhou");

Thread threadOne = new Thread(new Runnable() {
@Override
public void run() {

//修改list中下标为1的元素为baba
arrayList.set(1, "baba");
//删除元素
arrayList.remove(2);
arrayList.remove(3);
}
});

//保证在修改线程启动前获取迭代器
Iterator<String> itr = arrayList.iterator();
//启动线程
threadOne.start();

//等待子线程执行完毕
threadOne.join();

//迭代元素
while(itr.hasNext()){
System.out.println(itr.next());
}
}
}

主线程在子线程执行完毕后使用获取的迭代器遍历数组元素,从输出结果我们知道,在子线程里面进行的操作一个都没有生效,这就是迭代器弱一致性的体现。获取迭代器的操作必须在子线程操作之前进行。

                                                 CHAPTER-FIVE  OVER.......

                                                            PROCESS--->5/11

                ​​​​​​​        ​​​​​​​