Java 并发包中并发 List 源码剖析

    并发包中的 并发 List 只有 CopyOnWriteArrayList,它是个线程安全的 ArrayList,对其进行的修改都是在底层的一个复制的数组(快照📸【关于指定数据集合的一个完全可用拷贝】)上进行的,也就是使用了 写时复制 策略。

一、CopyOnWriteArrayList 类图

在这里插入图片描述
    每个 CopyOnWriteArrayList 类对象里有一个 array 数组对象用来存放具体元素,ReentrantLock 独占锁对象用来保证同时只有一个线程对 array 进行修改。

二、CopyOnWriteArrayList 主要方法源码分析

1、 初始化
  • 无参构造:
/**
 * Creates an empty list.
 */
public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
  • 有参构造:
    • 入参为集合,将集合里面的元素复制到 list
public CopyOnWriteArrayList(Collection<? extends E> c) {
    Object[] elements;
    if (c.getClass() == CopyOnWriteArrayList.class)
        elements = ((CopyOnWriteArrayList<?>)c).getArray();
    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);
}
    • 创建一个 list,其内部元素是入参 toCopyIn 的副本
public CopyOnWriteArrayList(E[] toCopyIn) {
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }

    

2、添加元素
public boolean add(E e) {

	// (1)获取独占锁
    final ReentrantLock lock = this.lock;
    
    lock.lock();
    try {
    	// (2)获取 array
        Object[] elements = getArray();

		// 复制 array 到新数组,添加新元素到新数组
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;

		// (3)使用新数组替换添加前数组
        setArray(newElements);
        return true;
    } finally {

		// 释放锁
        lock.unlock();
    }
}

    可以看到,(1)处获取独占锁,如果多个线程都调用 add 方法,则只有一个线程会获取到该锁,其他线程会被阻塞挂起直到锁释放。而且 获取 array 后,复制 array 到新数组,新数组的大小是原来数组大小增加 1 ,所以 CopyOnWriteArrayList 是 无界 list
(2):

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

(3):

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

    ✨ 而且可以看出,添加元素时,首先复制了一个快照,然后在快照上进行添加,而不是直接在原来数组上进行。✨
    

3、删除元素
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];

			// 分两次复制,index 之前 和 index 之后的 
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,numMoved);

			// 使用新数组代替老数组
            setArray(newElements);
        }
        return oldValue;
    } finally {

		// 释放锁
        lock.unlock();
    }
}

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

4、获取指定位置元素
public E get(int index) {
		// (1)
        return get(getArray(), index);
    }

(1):

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

     当线程调用 get 方法获取指定位置的元素时,分两步走, 首先获取 array 数组; 通过下标访问指定位置的元素,但是整个过程并没有加锁同步。

- 写时复制产生的弱一致性问题

    如下所示,当线程A 先获取到了 array {1,2,3},然后线程 B 拿到CPU,执行 remove ,删除掉1 ,删除的过程是:先拿到 ReetrantLock ,确保只有这个线程会删除元素,然后拷贝到 newArray,删除 “1” 后再将 array 指向 {2,3},而线程 A 仍是指向的 {1,2,3}。因此 线程A get(0) 取的元素仍是 1, 虽然 “1” 在线程 B 中已经被删除了。这就是 写时复制策略产生的 弱一致性 问题
在这里插入图片描述

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

    

5、修改指定元素
 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;

				// 拷贝原数组,在新数组上修改值,并设置新数组到 array
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } 

				// 如果指定位置的元素和新值一样,为了保证 volatile 语义,还是需要重新设置 array,虽然 array 的内容并没有改变
				else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }
- 弱一致性的迭代器

例👀 先看看迭代器的使用:

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

public class IteratorTest {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> arrayList=new CopyOnWriteArrayList<>();
        arrayList.add("hello");
        arrayList.add("jia");

        Iterator<String> itr=arrayList.iterator();
        while (itr.hasNext()){
            System.out.println(itr.next());
        }
    }
}

运行结果:
在这里插入图片描述
    hasNext() 方法用于判断列表中是否还有元素,next 方法则返回具体元素。 CopyOnWriteArrayList 中 迭代器的弱一致性 是指,返回迭代器后,其他线程对 list 的增 删 改 对迭代器是不可见的(因为增删改和迭代器操作的是两个数组)。

  • iterator() 源码:
 public Iterator<E> iterator() {
 		//(1)
        return new COWIterator<E>(getArray(), 0);
    }

(1):

static final class COWIterator<E> implements ListIterator<E> {
        // array 的快照版本
        private final Object[] snapshot;
        
        // 随后会被调用的要返回的数组下标
        /** Index of element to be returned by subsequent call to next.  */
        private int cursor;
        
private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }
  • hasnext() 源码:
		   // 是否遍历结束
           public boolean hasNext() {
            return cursor < snapshot.length;
        }

  • next() 源码:
 @SuppressWarnings("unchecked")

		// 获取元素
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

    可以看到,当调用 iterator 方法获取迭代器时,实际上会返回一个 COWIterator 对象,COWIterator 对象的 snapshot 变量保存了当前 list 的内容,cursor 是遍历 list 时数据的下标。如果在该线程使用返回的 迭代器 遍历元素的过程中,其他线程没有对 list 进行增 删 改,那么 snapshot 本身就是 list 的 array,但是,如果在遍历期间,其他线程对该 list 进行了增删改,那么 snapshot 就是快照了,因为增删改后 list 里的数组被新数组替换啦,而 snapshot 还引用着老数组。这就是 迭代器弱一致性的体现。
    
例👀 多线程下迭代器的弱一致性的效果:

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

public class IteratorTest2 {
    private static volatile CopyOnWriteArrayList<String> arrayList=new CopyOnWriteArrayList<>();
package DataStractures.ArrayListPack;

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

public class IteratorTest2 {
    private static volatile CopyOnWriteArrayList<String> arrayList=new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        arrayList.add("hello");
        arrayList.add("jia");
        arrayList.add("welcome");
        arrayList.add("to");
        arrayList.add("ohh");

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

                // 修改 list 中下标为1 的元素为 bin
                arrayList.set(1,"bin");

                // 删除元素
                arrayList.remove(2);
                arrayList.remove(3);
            }
        });

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

        // 启动 修改线程
        threadOne.start();

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

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

运行结果:
在这里插入图片描述
    从运行结果看到,获取迭代器,是在子线程运行之前执行的,而在子线程里的操作,一个都没有生效,这就是迭代器弱一致性的表现。

    
🎭 总结 :
    CopyOnWriteArrayList 使用 写时复制 的策略,来保证 list 的一致性,而 获取 - 修改 - 写入,这三步操作不是原子性的,所以,在 增 删 改 的过程中都使用了 独占锁,来保证在某个时刻,只有一个线程能对 list 进行操作,是线程安全的,但是 写时复制 会导致 弱一致性问题。另外,CopyOnWriteArrayList 提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对 list 的修改是不可见的,迭代器遍历的数组是一个快照。而且,CopyOnWriteArraySet 的底层是 使用 CopyOnWriteArrayList 实现的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值