前言
线程安全的 List 还有 Vector
和 SynchronizedList
,本文将介绍下这两个 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 都插入十万个数,看看它们用的时间,用的测试代码如下(初始化代码省略了):
代码很简单,我就贴个截图,反正就是插十万个数,统计时间,结果如下:
可以看见,CopyOnWriteArrayList 的插入效率及其低下,毕竟每次插入都复制一遍数组嘛。
然后虽然 SynchronizedList 看着好像比另外三个慢一点,其实是因为它第一个插入,把另外三个放在第一个运行,也会慢一点(可能是 CPU 什么的冷启动需要预热?),它们三个插入时间基本是差不多的。
三个并发 List 综合比较
因为这三个并发的 List 的源码我都看过了,所以根据自己的理解对它们进行了比较,如下表:
综合比较 | 读的效率 | 写的效率 | 是否要扩容 | 强一致性 |
---|---|---|---|---|
CopyOnWriteArrayList | 高,不用加锁 | 很低,要复制数组 | 不需要 | 没有 |
SynchronizedList | 略低,要加锁 | 还行 | 看传入的 list 情况 | 有 |
Vector | 略低,要加锁 | 还行 | 要扩容 | 有 |
总结
本文对Vector
和 SynchronizedList
的基本结构和如何实现线程安全从源码角度进行了解答,并在最后一节给出了几种 List 容器的比较。