集合框架——List

List

特点

List是一个有序的集合 
List允许存储项的值为空
List可以动态增长,根据实际存储的数据的长度自动增长List的长度

ArrayList

介绍

  1. ArrayList不是线程安全的
  2. 底层是数组,初始大小为10
  3. 插入时会判断数组容量是否足够,不够的话会进行扩容
  4. 所谓扩容就是新建一个新的数组,然后将老的数据里面的元素复制到新的数组里面
  5. ArrayList的底层是数组,可以通过下标索引直接查找到指定位置的元素,因此查找效率高。
  6. 增加和删除就涉及到数组元素的移动,所以会比较慢。

线程安全

转为线程安全常用的有两种方式:

  1. 直接加锁synchronized
  2. 使用Collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,内部是使用synchronized加锁方式,这种加锁力度更小、更精细。
    12

扩容

数组进行扩容:当增加数据的时候,如果ArrayList的大小已经不满足需求时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。
这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
4

CopyOnWriteArrayList

介绍

  1. 实现了List接口
  2. 内部持有一个ReentrantLock lock = new ReentrantLock();
  3. 底层是用volatile transient声明的数组 array
  4. 读写分离,写时复制出一个新的数组,完成插入、修改或者移除操作后将新数组赋值给array
  5. 读的时候不需要加锁,如果读的时候有多个线程正在向CopyOnWriteArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的CopyOnWriteArrayList。
    总结一句话:底层是使用ReentrantLock加锁 + volatile声明线程之间共享数组 + 写时复制操作机制 + 读写分离思想

应用场景

CopyOnWrite并发容器用于读多写少的并发场景。

写时复制

CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。

CopyOnWriteArrayList缺点

CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。
6. 内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。
针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。
7. 数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

数组越界风险

一般来讲我们使用时,会用一个线程向容器中添加元素,一个线程来读取元素,而读取的操作往往更加频繁。写操作加锁保证了线程安全,读写分离保证了读操作的效率,简直完美。
但是想象一下如果这时候有第三个线程进行删除元素操作,读线程去读取容器中最后一个元素,读之前的时候容器大小为i,当去读的时候删除线程突然删除了一个元素,这个时候容器大小变为了i-1,读线程仍然去读取第i个元素,这时候就会发生数组越界。
可以看出CopyOnWriteArrayList并不是完全意义上的线程安全,如果涉及到remove操作,一定要谨慎处理。

源码

增加
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();
    }
}
删除
public E remove(int index) {
    final ReentrantLock lock = this.lock;
    //获得锁
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        E oldValue = get(elements, index);
        int numMoved = len - index - 1;
        if (numMoved == 0)
            //如果删除的元素是最后一个,直接复制该元素前的所有元素到新的数组
            setArray(Arrays.copyOf(elements, len - 1));
        else {
            //创建新的数组
            Object[] newElements = new Object[len - 1];
            //将index+1至最后一个元素向前移动一格
            System.arraycopy(elements, 0, newElements, 0, index);
            System.arraycopy(elements, index + 1, newElements, index,
                             numMoved);
            setArray(newElements);
        }
        return oldValue;
    } finally {
        lock.unlock();
    }
}
修改
public E set(int index, E element) {
    final ReentrantLock lock = this.lock;
    //获得锁
    lock.lock();
    try {
        Object[] elements = getArray();
        E oldValue = get(elements, index);

        if (oldValue != element) {
            int len = elements.length;
            //创建新数组
            Object[] newElements = Arrays.copyOf(elements, len);
            //替换元素
            newElements[index] = element;
            //将新数组指向原来的引用
            setArray(newElements);
        } else {
            // Not quite a no-op; ensures volatile write semantics
            setArray(elements);
        }
        return oldValue;
    } finally {
        //释放锁
        lock.unlock();
    }
}
查询
//直接获取index对应的元素
public E get(int index) {return get(getArray(), index);}
private E get(Object[] a, int index) {return (E) a[index];}

LinkedList

介绍

  1. LinkedList 是一个继承于AbstractSequentialList的双向链表。它也可以被当作堆栈、队列或双端队列进行操作。
  2. LinkedList 实现 List 接口,能对它进行队列操作。
  3. LinkedList 实现 Deque 接口,即能将LinkedList当作双端队列使用。
  4. LinkedList 实现了Cloneable接口,即覆盖了函数clone(),能克隆。
  5. LinkedList 实现java.io.Serializable接口,这意味着LinkedList支持序列化,能通过序列化去传输.
  6. LinkedList 是非同步的。线程不安全的
  7. LinkedList不存在LinkedList容量不足的问题。
  8. LinkedList没有扩容机制

实现原理

LinkedList底层的数据结构是基于双向循环链表的,且头结点中不存放数据,如下:
在这里插入图片描述
既然是双向链表,那么必定存在一种数据结构——我们可以称之为节点,节点实例保存业务数据,前一个节点的位置信息和后一个节点位置信息,如下图所示:
在这里插入图片描述

利用LinkedList实现栈操作

public class Stack<T>
{
    private LinkedList<T> stack;
    
    //无参构造函数
    public Stack()
    {
        stack=new LinkedList<T>();
    }
    //构造一个包含指定collection中所有元素的栈
    public Stack(Collection<? extends T> c)
    {
        stack=new LinkedList<T>(c);
    }
    //入栈
    public void push(T t)
    {
        stack.addFirst(t);
    }
    //出栈
    public T pull()
    {
        return stack.remove();
    }
    //栈是否为空
     boolean isEmpty()
     {
         return stack.isEmpty();
     }
     
     //打印栈元素
     public void display()
     {
         for(Object o:stack)
             System.out.println(o);
     }
}

总结

 1. 插入数据很快。先是在双向链表中找到要插入节点的位置index,找到之后,再插入一个新节点。双向链表查找index位置的节点时,有一个加速动作:若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。
 2. 删除数据很快。先是在双向链表中找到要插入节点的位置index,找到之后,进行如下操作:node.previous.next =node.next;node.next.previous = node.previous;node = null 。查找节点过程和插入一样。
 3. 获取数据很慢,需要从Head节点进行查找。	
 4. 遍历数据很慢,每次获取数据都需要从头开始。 	
 5. LinkedList实际上是通过双向链表去实现的。它包含一个非常重要的内部类:Entry。Entry是双向链表节点所对应的数据结构,它包括的属性有:当前节点所包含的值,上一个节点,下一个节点。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值