一、介绍
前面我们介绍了java集合中的两种List实现类:基于数组实现的ArrayList
和基于双向链表实现的LinkedList
,从这两个类的源码中我们注意到,他们的方法实现都是线程不安全的,在多个线程共享一个实例的情况下会出现无法解决的问题。那么我们该如何避免问题的发生以保证线程安全呢?
java为我们提供了一些解决方案:
-
使用
Collections.synchronizedList(List<T> list)
方法,该方法对传入的List对象进行包装,返回一个线程安全的List
。保证线程安全的原理就是在我们调用add()
、remove()
等方法时通过使用synchronized
代码块对方法进行包装,从而实现线程安全。// 线程不安全的List对象 List<Integer> unSynchronizedList = new ArrayList<>(); // 线程安全的List对象 List<Integer> synchronizedList = Collections.synchronizedList(unSynchronizedList);
-
使用
CopyOnWriteArrayList
,顾名思义,该类采用写入时复制的方法并结合ReentrantLock可重入锁来实现线程安全。// 线程安全的List对象 List<Integer> list = new CopyOnWriteArrayList<>();
-
使用
Vector
,本篇文章的主角,通过使用synchronized
关键字实现线程安全。// 线程安全的List对象 List<Integer> list = new Vector<>();
在java的早期版本中,为了解决ArrayList
的并发问题,在java1.2中就开始引入了Vector
,但是由于synchronized
关键字过于重量级且不可控制,容易导致多线程死锁等问题的发生,相当于伤敌一千自损八百,捡了西瓜丢了芝麻。因此从java1.5开始,引入新的保证线程安全的集合实现类CopyOnWriteArrayList
。
虽然java已经不建议我们使用Vector
了,但是由于使用简单,又有哪位同学在并发学习中选择直接绕过它呢?
下面我们看一下Vector的UML图:
① 实现了List
接口,表示实现了List接口所定义的规范
② 继承自AbstractList
抽象类,AbstractList
同时也实现了List
接口,表示继承了AbstractList
对List
的默认实现
① 实现了Cloneable
接口,表示具有克隆的功能
② 实现了Serializable
接口,表示具有序列化的功能
③ 实现了RandomAccess
接口,表示具有随机访问的功能
从UML图中可以看出,Vector与ArrayList的继承关系完全一致。因此我们可以先假设这两个类的功能在使用上也完全一致,但要注意的是Vector是线程安全的,而ArrayList是线程不安全的。
注意:上面提到Vector是为了解决ArrayList线程不安全的问题而引入的,我们可以认为Vector是线程安全的ArrayList,在下面的学习中,我们应当将这两个类的不同点作为学习重点。
关于ArrayList源码的学习,请看往期文章:ArrayList源码阅读
二、成员变量
// 对象数组,具有实际意义,用于保存Vector的元素,由此说明Vector是通过数组保存元素的
protected Object[] elementData;
// elementData中对象的数量
protected int elementCount;
// elementData数组在扩容时的增量
protected int capacityIncrement;
// 继承父类AbstractList的变量
// 在使用迭代器遍历过程中,对结构修改的次数,通过该字段可以实现fail-fast快速失败
protected transient int modCount = 0;
三、构造函数
①指定初始容量和扩容增量
// 该构造函数指定Vector实例的初始容量和扩容时的容量的增量
public Vector(int initialCapacity, int capacityIncrement) {
// 调用父类的无参构造空方法,
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
②指定初始容量
// 指定初始容量,增量为缺省值0。在扩容部分我们可以看到,当增量为0时,实际增量为原始容量,即扩容后的容量为扩容前的2倍
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
③无参构造
// 默认初始容量为10,增量为缺省值0
public Vector() {
this(10);
}
④通过一个集合构造
// 该构造函数与ArrayList的构造函数的实现一致
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的源码来看扩容原理,可以发现它的实现与ArrayList的扩容实现几乎完全一致,唯一不同的地方我把源码贴在下面:
// ArrayList的扩容实现, 扩容后的容量为扩容前容量的1.5倍
private void grow(int minCapacity) {
// ...
int newCapacity = oldCapacity + (oldCapacity >> 1);
// ...
}
// Vector的扩容实现
// 如果在调用构造函数时指定的扩容增量大于0,则扩容后的容量=扩容前容量+扩容增量
// 否则,扩容后的容量=扩容前容量*2
private void grow(int minCapacity) {
// ...
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
// ...
}
五、补充说明
Vector从源码上来看,无论是增加元素、删除元素、修改元素、获取元素等方法,与ArrayList大同小异,几乎没有差别,建议读者在学习时以ArrayList作为重点去学习它的源码,然后再以它为参考,对Vector有个基本了解就行了。
再说说面试,Vector的重点知识其实很少很少,一般面试时面试官是不会问的,那么如果问了,可能会找出哪些问题?
①Vector和ArrayList的相同点是什么,不同点是什么,各自的使用场景是什么。
②以Vector线程安全为引子,对synchronized
关键字的原理大文特问。
六、Vector和ArrayList的主要对比
相同点 | 不同点 | |
---|---|---|
底层结构 | 数组 | |
初始容量 | 10 | |
构造函数 | 1.都可以指定初始容量构造 2.都可以通过集合构造 | 1.ArrayList有三个,Vector有四个 2.ArrayList可以通过集合、初始容量构造;Vector可以通过集合、初始容量、扩容增量构造 |
扩容 | ArrayList扩容后的容量为扩容前容量的1.5倍 Vector扩容后的容量为 扩容前容量+扩容增量 或 扩容前容量*2 |
至此,java集合中List
接口的常用实现ArrayList
、LinkedList
、Vector
已经通关了,接下来我们一起进入Queue
接口的实现。
我们下期再会。