List是经常使用的集合之一。List类层次结构如下:
其中,Vector、Stack已弃用,仅具有历史意义,仅需了解其原理实现。在非线程安全场景下,可以选用ArrayList和LinkedList,对于线程安全场景,可以选用CopyOnWriteArrayList。
Vector
Vector是 JDK 1.0 版本提供的同步容器类,并在JDK 1.2中实现了Collection接口。随着JDK版本的不断更新,这个类已经逐渐被弃用。
1. Vector 基于动态数组实现
Vector 基于动态数组实现,其结构设计如下:
(1) Object[] elementData 数组保存添加到Vector中的元素。elementData是个动态数组,默认值大小10。随着Vector中元素的增加,Vector的容量也会,根据capacityIncrement动态增长。因为在扩容时,必须保证有连续的内存空间,所以会将原数据拷贝到新申请的内存空间中。
(2) elementCount 是数组的实际大小。
(3) capacityIncrement 是动态数组的增长系数。在创建Vector时,可指定capacityIncrement的大小。当capacityIncrement值小于等于0或者未设置时,Vector中将增长一倍(默认增长一倍)。
(4) Vector的克隆函数,会将全部元素克隆到一个数组中。
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity); // 将全部元素克隆到新数组
}
2. Vector是线程安全
Vector作为同步容器类,通过将状态封住起来,并对每个公有方法进行同步,使得每次只有一个线程能够访问容器的状态(使用synchronized关键字修饰)。
代码示例如下:
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
protected Object[] elementData;
protected int elementCount;
protected int capacityIncrement;
// 公有方法均使用 synchronized 修饰
public synchronized int capacity() {
return elementData.length;
}
public synchronized int size() {
return elementCount;
}
// 其他公共方法
...
}
3. Vector上某些复合操作存在非线程安全问题
Vector作为同步容器类无法保证“绝对线程安全”(同步容器类在执行某些场景的复合操作时,需要额外的客户端加锁来保护),且不支持并发操作。
容器上常见的复合操作有迭代(反复访问元素,主动遍历完容器中所有元素),跳转(根据指定顺序找到当前元素的下一个元素)以及条件运算,等等。 同步容器类中,容器被多个线程并发修改时,可能会出现意料之外的行为。实例如下:
对Vector 封装两个方法:getLast、deleteLast,都会“先检查再执行”操作。完整代码如下:
public static Object getLast(Vector vec){
int lastIndex=vec.size()-1;
return vec.get(lastIndex);
}
public static Object deleteLast(Vector vec){
int lastIndex=vec.size()-1;
return vec.remove(lastIndex);
}
从方法调用的角度来看,如果线程A在10个元素中调用getLast,线程B调用deleteLast。当线程B在线程A读取lastIndex后执行时,线程A在执行getLast时,将抛出ArrayIndexOutOfBoundsException异常(数组越界)。
所以, 同步容器类要遵循同步策略,即客户端加锁。示例代码如下:
public static Object getLast(Vector vec){
synchronized(vec){
int lastIndex=vec.size()-1;
return vec.get(lastIndex);
}
}
public static Object deleteLast(Vector vec){
synchronized(vec){
int lastIndex=vec.size()-1;
return vec.remove(lastIndex);
}
}
Stack
Stack继承Vector,仅封装了部分Stack接口。关键源码如下:
public class Stack<E> extends Vector<E> {
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;
}
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
}
ArrayList
ArrayList 是一个动态数组。与Java中的内置的数组相比,它的容量能动态增长。
ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们可以通过元素的序号快速获取元素对象;这就是快速随机访问。
1.ArrayList基于动态数组实现
ArrayList实现了动态数组功能。ArrayList基于数组实现了动态数组。相关定义如下:
transient Object[] elementData; // non-private to simplify nested class access
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
// 动态增长为原来的一半
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
相比于Map的增长一倍,ArrayList动态增长的长度是之前长度的一半。
ArrayList的默认长度是10。
/**
* Default initial capacity.
*/
private static final int DEFAULT_CAPACITY = 10;
因为是动态数组,所以ArrayList理论上不会出现数组越界的情况。
2.ArrayList是非线程安全
ArrayList是非线程安全,即任一时刻可以有多个线程同时写ArrayList,可能会导致数据的不一致。
LinkedList
LinkedList实现了Queue接口。
LinkedList提供Stack操作接口。
1. LinkedList基于双链表实现
(1)LinkedList 使用 Node 存储节点信息
Node定义如下:
private static class Node<E> {
E item;
// 下一个节点
Node<E> next;
// 上一个节点
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
(2)LinkedList可快速访问头节点和尾节点
LinkedList是基于链表实现,无法实现基于索引的快速查找。但是,LinkedList记录了头节点和尾节点的信息,可以快速的定位头尾节点。
/**
* Pointer to first node.
* Invariant: (first == null && last == null) ||
* (first.prev == null && first.item != null)
*/
transient Node<E> first;
/**
* Pointer to last node.
* Invariant: (first == null && last == null) ||
* (last.next == null && last.item != null)
*/
transient Node<E> last;
(3)LinkedList实现了双端队列接口
LinkedList实现了双端队列(Deque)接口,支持将其当做双端队列进行使用。
(4)LinkedList可作为队列使用
Java并未直接实现Queue的实体类,而是在LinkedList实现了Queue接口。
(5)LinkedList可作为栈使用
相比于基于Vector实现的Stack,LinkedList虽然不支持线程同步,但是非多线程场景下,其访问性能要高很多。所以非多线程场景下,LinkedList是栈的首选。
2.LinkedList是非线程安全
LinkedList非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。
CopyOnWriteArrayList
CopyOnWriteArrayList是ArrayList的线程安全版本。CopyOnWriteArrayList是在有写操作的时候copy一份数据,然后写完再设置成新的数据。CopyOnWriteArrayList适用于读多写的并发场景。
1. 使用COW技术+重入锁保证线程安全
CopyOnWriteArrayList使用了ReentrantLock来支持并发操作,array就是实际存放数据的数组对象。ReentrantLock是一种支持重入的独占锁,任意时刻只允许一个线程获得锁,所以可以安全的并发去写数组。对应成员变量如下:
// 重入锁保证写操作互斥
final transient ReentrantLock lock = new ReentrantLock();
// volatile保证读可见性
private transient volatile Object[] array;
向CopyOnWriteArrayList添加元素,可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 加锁,保证只有一个线程进入
lock.lock();
try {
// 获得当前数组对象
Object[] elements = getArray();
int len = elements.length;
// 拷贝到一个新的数组中
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 追加新元素
newElements[len] = e;
// 使用新数组对象更新当前数组的引用
setArray(newElements);
return true;
} finally {
// 解锁
lock.unlock();
}
}
读操作时不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据(可能出现脏读),因为写的时候不会锁住旧的ArrayList。
public E get(int index) {
return get(getArray(), index);
}
2. CopyOnWriteArrayList是线程安全
参考
https://www.cnblogs.com/msymm/p/9873551.html
https://www.cnblogs.com/skywang12345/p/3308556.html
https://www.cnblogs.com/skywang12345/p/3308807.html
https://www.jianshu.com/p/cd7a73e6bd78
https://zhuanlan.zhihu.com/p/59601301
https://www.jianshu.com/p/cd7a73e6bd78
原创不易,如果本文对您有帮助,欢迎关注我,谢谢 ~_~