线程安全的List(二):Vector && SynchronizedList

前言

  线程安全的 List 还有 VectorSynchronizedList,本文将介绍下这两个 List 容器,看看它们各自的结构,方法,以及它们是怎么实现线程安全的。本文承接作者的上一篇博客:线程安全的List(一):CopyOnWriteArrayList 源码解析

Vector

  Vector 的源码和 ArrayList 的源码比较类似,太具体的源码我就不一一详细解析了,大家感兴趣可以看我的这篇博客:ArrayList源码学习(一):初始化,扩容以及增删改查

基本结构

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    protected Object[] elementData;
    protected int elementCount;
    protected int capacityIncrement;
}
复制代码

  Vector 继承了 AbstractList 类,实现了 List,RandomAccess,Cloneable 等接口。

  主要属性就这三个,elemantData,存储元素数据的数组;elementCount,数组实际上有多少元素,因为由于扩容等操作,elementData 这个数组的长度可能比实际上元素的个数多;capacityIncrement,每次扩容时增加多少容量。

初始化

// 指定初始容量和扩容增量
public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];
    this.capacityIncrement = capacityIncrement;
}
// 指定初始容量
public Vector(int initialCapacity) {
    this(initialCapacity, 0);
}
// 无参
public Vector() {
    this(10);
}
// 用集合来初始化
public Vector(Collection<? extends E> c) {
    Object[] a = c.toArray();
    elementCount = a.length;
    if (c.getClass() == ArrayList.class) {
        elementData = a;
    } else {
        elementData = Arrays.copyOf(a, elementCount, Object[].class);
    }
}
复制代码

  初始化也很简单,可以选择是否指定初始容量,扩容增量,也可以用集合来初始化。

如何确保线程安全

  咱们也不多废话了,直接看它是怎么保证线程安全的。我把 Vector 每个方法都看了看,它的方法基本上可以分为以下四类:

  • 方法本身是 synchronized 的:
// 特定索引处的元素
public synchronized E elementAt(int index) {
    // 省略具体代码
}  
// set 方法
public synchronized E set(int index, E element) {
    // 省略具体代码
}
复制代码
  • 关键代码在 synchronized 代码块里:
public boolean addAll(Collection<? extends E> c) {
    Object[] a = c.toArray();
    modCount++;
    int numNew = a.length;
    if (numNew == 0)
        return false;
    // 判断满足条件,进入 synchronized 代码块
    synchronized (this) {
       // 省略具体代码
    }
}
复制代码
  • 被调用的方法是 synchronized 的:
public boolean contains(Object o) {
    return indexOf(o, 0) >= 0;
}  
public synchronized int indexOf(Object o, int index) {
    // 省略具体代码
}
复制代码
  • 发起调用的方法是 synchronized 的:
public synchronized boolean add(E e) {
    modCount++;
    // 调用 synchronized 方法
    add(e, elementData, elementCount);
    return true;
}
private void add(E e, Object[] elementData, int s) {
    // 省略具体代码
}
复制代码

  反正不管怎么调用,都一样,所有方法最终想完成操作,都得抢到锁,同一时间只有一个线程可以进行操作,自然这些方法都是线程安全的。

  Vector 这些方法单个使用肯定没问题,不过如果你是多个方法一块使用,比方说:

    int lastIndex  = v.size()-1;
    v.remove(lastIndex);
复制代码

  虽然单个方法都是线程安全的,不过可能你 remove 的时候,另一个线程也 remove,人家先抢到锁了,那这个代码会抛出数组越界的异常。所以复合操作还是得另外加锁的。。。

Vector 的应用:Stack

  之前某天我闲来无事,看了看 Java.Util 下面有哪些类,看到这个早就不被推荐使用的 Stack,遂点进去看了看,好家伙,我发现 Stack 底层竟然是 Vector!

public class Stack<E> extends Vector<E> {
    public Stack() {
    }  
}
复制代码

  Stack 自己这构造函数啥也没有,所以它其实就是个有 push,pop 等方法的 Vector:

public E push(E item) {
    addElement(item);
    return item;
}
public synchronized E pop() {
    E       obj;
    int     len = size();
    obj = peek();
    removeElementAt(len - 1);
    return obj;
}
// 其它方法
复制代码

  还记得上一小节我们说int lastIndex = v.size()-1; v.remove(lastIndex);这个代码不线程安全吗?看看,Stack 的 pop 就把这两个操作封装在一个 synchronized 函数里了。。。

  复制一段 Stack 作者的注释:

* <p>A more complete and consistent set of LIFO stack operations is
* provided by the {@link Deque} interface and its implementations, which
* should be used in preference to this class.
复制代码

  就是说 Deque 这个接口的实现类是作为栈的更好的选择。想想也是,有时候咱们用个栈可能没有线程安全的需求,结果 Stack 这每次都 synchronized,多慢啊,而且数组作为 Stack 的底层,还要扩容。

SynchronizedList

创建

  先看下如何创建一个 SynchronizedList:

List<Integer> list = new ArrayList<>();
List<Integer> list1 = Collections.synchronizedList(list);
复制代码

  将一个 List 接口的实现类的实例传入构造函数中。

public static <T> List<T> synchronizedList(List<T> list) {
    return (list instanceof RandomAccess ?
            new SynchronizedRandomAccessList<>(list) :
            new SynchronizedList<>(list));
}
复制代码

  可见,根据是否实现了 Access 接口,会创建不同的 SynchronizedList。

基本结构和初始化

  先看下 SynchronizedList:

static class SynchronizedList<E>
    extends SynchronizedCollection<E>
    implements List<E> {
    @java.io.Serial
    private static final long serialVersionUID = -7754090372962971524L;
    // 传入的 list
    final List<E> list;
    SynchronizedList(List<E> list) {
        super(list);
        this.list = list;
    }
    SynchronizedList(List<E> list, Object mutex) {
        super(list, mutex);
        this.list = list;
    }
复制代码

  SynchronizedList 是 Collections 类的静态内部类,可以看出,它继承了 SynchronizedCollection 类,实现了 List 接口。重要属性只有一个:final List<E> list;,初始化时,调用this.list = list;来设置自己的 list 属性。

  它继承的父类,即 SynchronizedCollection:

static class SynchronizedCollection<E> implements Collection<E>, Serializable {
    @java.io.Serial
    private static final long serialVersionUID = 3053995032091335093L;

    @SuppressWarnings("serial") // Conditionally serializable
    final Collection<E> c;  // Backing Collection
    @SuppressWarnings("serial") // Conditionally serializable
    final Object mutex;     // Object on which to synchronize

    SynchronizedCollection(Collection<E> c) {
        this.c = Objects.requireNonNull(c);
        mutex = this;
    }

    SynchronizedCollection(Collection<E> c, Object mutex) {
        this.c = Objects.requireNonNull(c);
        this.mutex = Objects.requireNonNull(mutex);
    }
复制代码

  可以看到,这里有一个 mutex 变量,作为锁,然后如果 SynchronizedList 调用super(list);,那mutex = this;,否则,this.mutex = Objects.requireNonNull(mutex);,mutex 就用传入的 mutex 初始化。

  再看 SynchronizedRandomAccessList:

static class SynchronizedRandomAccessList<E>
    extends SynchronizedList<E>
    implements RandomAccess {

    SynchronizedRandomAccessList(List<E> list) {
        super(list);
    }

    SynchronizedRandomAccessList(List<E> list, Object mutex) {
        super(list, mutex);
    }
复制代码

  这个类本身没有什么东西,主要还是依靠它的父类,SynchronizedList。

  总之,初始化会将传入的 List 接口实现类的实例赋给 SynchronizedList 的 list 属性,并且会在 SynchronizedCollection 中进行 mutex(作为 synchronized 锁的对象) 属性的设置。

如何确保线程安全

  SynchronizedList 也实现了 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);}
}
public E remove(int index) {
    synchronized (mutex) {return list.remove(index);}
}
public int indexOf(Object o) {
    synchronized (mutex) {return list.indexOf(o);}
}
// 其它方法
复制代码

  SynchronizedList 里的方法都是这样,将操作放于 synchronized 代码块里,再借助创建时传入的 list,调用其对应方法,完成操作。所以 SynchronizedList 就相当于一个包装类,把传入的 list 包在里面,把操作包在 synchronized 代码块里,所以这些基本方法当然是线程安全的。

  不过 SynchronizedList 的迭代器不是线程安全的,代码如下:

public ListIterator<E> listIterator() {
    return list.listIterator(); // Must be manually synched by user
}
public ListIterator<E> listIterator(int index) {
    return list.listIterator(index); // Must be manually synched by user
}
复制代码

  创建的迭代器只是原 list 的迭代器,肯定没什么线程安全可言,所以作者在后面注释必须由用户手动同步:

Must be manually synched by user
复制代码

几个 List 的比较

插入性能比较

  这一小节满足作者的好奇心 o( ̄▽ ̄)ブ ,作者对 LinkedList,ArrayList,Vector,CopyOnWriteArrayList 和 SynchronizedList 都插入十万个数,看看它们用的时间,用的测试代码如下(初始化代码省略了):

image.png

  代码很简单,我就贴个截图,反正就是插十万个数,统计时间,结果如下:

image.png

  可以看见,CopyOnWriteArrayList 的插入效率及其低下,毕竟每次插入都复制一遍数组嘛。

  然后虽然 SynchronizedList 看着好像比另外三个慢一点,其实是因为它第一个插入,把另外三个放在第一个运行,也会慢一点(可能是 CPU 什么的冷启动需要预热?),它们三个插入时间基本是差不多的。

三个并发 List 综合比较

  因为这三个并发的 List 的源码我都看过了,所以根据自己的理解对它们进行了比较,如下表:

综合比较 读的效率 写的效率 是否要扩容 强一致性
CopyOnWriteArrayList 高,不用加锁 很低,要复制数组 不需要 没有
SynchronizedList 略低,要加锁 还行 看传入的 list 情况
Vector 略低,要加锁 还行 要扩容

总结

  本文对VectorSynchronizedList的基本结构和如何实现线程安全从源码角度进行了解答,并在最后一节给出了几种 List 容器的比较。

猜你喜欢

转载自juejin.im/post/7041036887372333069