ArrayList和LinkedList

两盒软妹~` 提交于 2020-11-01 18:08:19

微信公众号:MyClass社区
如有问题或建议,请公众号留言

1.ArrayList并发测试

写在前边,越是简单的数据结构,我们有时候就是容易忽略。并发下的ArrayList会出现什么情况,大家可能会说因为并发扩容可能抛数据越界异常,真的是这样吗?我们进行简单测试,分析一下:

@Test
    public void conTestArrayListSizeAdd() throws Exception 
{
        List<Integer> list = new ArrayList<>();
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int threadNum = 0; threadNum < 10; threadNum++) {
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10; i++) {
                        list.add(size++);
                    }
            }
            });
        }
        Thread.sleep(1000);
        System.out.println("size"+list.size()+",list:"+list);
        executorService.shutdown();
    }

预期结果:

实际情况:发现很多都是空,这是啥情况?这里添加了一个size++,进行比较衬托效果。

分析:

    //添加一个元素
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    //扩容1.5倍 (oldCapacity + oldCapacity >> 1)
    private void grow(int minCapacity) {
        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);
    }

其中很明显的线程不安全的非原子操作:size++,扩容过程中,数组会扩充很多null数据槽,elementData[size++]操作会填充数据,所以不难分析出这个结果:扩容之后,size++并发下赋值的位置就会错乱。数组越界其实分析并发下也是会出现的,但是跑了很多我这没有出现,大家可以自己测试一下。

2.再看看LinkedList

LinkedList是基于双向链表实现的,所以它的特征基本上是链表相关的特性:

    //1.双链表结构
    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.添加元素
    public boolean add(E var1) {
        this.linkLast(var1);
        return true;
    }
    //last节点赋值
    void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }
    //3.get获取节点node(index)
    Node<E> node(int index) {
         //判断是从前遍历还是从尾部遍历
         if (index < (size >> 1)) {
             Node<E> x = first;
             for (int i = 0; i < index; i++)
                 x = x.next;
             return x;
         } else {
             Node<E> x = last;
             for (int i = size - 1; i > index; i--)
                 x = x.prev;
             return x;
         }
     }

LinkedList添加元素时,会在尾节点进行替换,或者add(index,e) ,set(index,e)都是在相应的节点进行替换,所以并发情况下,节点赋值可能被覆盖,同样会出现错乱的现象。

    //4.往链表中间插入元素
    public void add(int var1, E var2) {
        this.checkPositionIndex(var1);
        if (var1 == this.size) {
            this.linkLast(var2);
        } else {
            //如果不是尾节点,需要获取遍历链表获取index的节点,
            this.linkBefore(var2, this.node(var1));
        }
    }
    //插入元素,替换old节点的前节点为newNode,old的前节点的next为newNode,完成插入
    void linkBefore(E var1, LinkedList.Node<E> var2) {
            LinkedList.Node var3 = var2.prev;
            LinkedList.Node var4 = new LinkedList.Node(var3, var1, var2);
            var2.prev = var4;
            if (var3 == null) {
                this.first = var4;
            } else {
                var3.next = var4;
            }
            ++this.size;
            ++this.modCount;
        }

LinkedList 添加元素:add(index,e),则会遍历链表获取index节点的位置,所以插入的时候也是需要遍历index节点位置,再进行节点前后替换,完成插入操作,所以LinkedList的插入和索引效率不能一概而论,以偏概全。

3.简单总结

1.ArrayList底层是一个动态Object数组,数组默认大小,初始值为10,如果需要扩容则会扩充1.5倍。1.6是(原size×3)/2。,后1.7之后优化为(原size+原size>>1);
2.ArrayList线程不安全,会有并发扩容问题,多线程赋值也会出现不一致,数组越界异常,还有会出现null现象;
3.LinkedList基于双链表的数据结构,线程不安全。

ArrayList&LinkedList索引和插入效率分析:

     索引:对于随机访问get和set,ArrayList绝对优于LinkedList,因为LinkedList要遍历链表来获取节点时间复杂度是O(n),而arrayList直接是数组下表定位,时间复杂O(1)。所以LinkedList的get就相对非常消耗资源,除非位置离头部尾很近,遍历时间很快。
     插入:对于新增和删除操作add和remove,LinedList比较占优势,因为ArrayList往中间add操作或者需要扩容操作是调用Array.copy复制数组,Array.copy是操作系统的内存操作,很耗性能;LinedList只需要在最后一个节点挂上节点,或者中间添加或者删除,都只是前后节点的引用赋值的改变,除了遍历没耗性能的操作。

4.并发怎么用

以上jdk自带arrayList和linkedList都是线程不安全的,如果非要在并发情况下使用这两种数据结构:

      1.建议加锁来保证线程安全;

      2.或者使用Vector,方法都加了synchronized synchronized保证了线程安全,但是1.6以后基本都不再使用了;

      3.还有一种方式可以 new SynchronizedCollection(List),或者使用Collections.synchronizedList(new LinkedList()),本质都是加锁;

      4.以上都是利用synchronized进行加锁保证的,性能不是最好的,一般的建议使用下面并发阻塞队列操作,例如ConcurrentLinkedQueue阻塞队列,底层实现CAS算法操作,保证了线程安全。    

欢迎关注微信公众号:MyClass社区,查看精彩历史。

本文分享自微信公众号 - MyClass社区(MyClass_ZZ)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!