Java List

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

原创不易,如果本文对您有帮助,欢迎关注我,谢谢 ~_~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值