美文网首页
几种Java同步List对比

几种Java同步List对比

作者: yaco | 来源:发表于2020-07-18 13:34 被阅读0次

    CopyOnWrite容器

    基本思想

    Copy-On-Write,写入时复制,这个技术,准确的说应该是一种思想,在很多系统设计上都会用到,今天我们来谈一谈Java语言中,JDK运用这种写入时复制的思想的数据结构/容器,CopyOnWriteArrayList。

    CopyOnWriteArrayList,是一个写入时复制的容器,它是如何工作的呢?简单来说,就是平时查询的时候,都不需要加锁,随便访问,只有在写入/删除的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。这点要跟读写锁区分一下。

    我们来看看CopyOnWriteArrayList的add方法源码

    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();
        }
    }
    
    • 首先可以看到CopyOnWriteArrayList中用到了ReentrantLock进行加锁,
    • 添加元素时,保证同时只有1个线程进行变更,在变更的时候,先拷贝出来一个副本,先操作这个副本,操作完成后,再把现有的数据替换成这个副本。

    再来看一下CopyOnWriteArraySet源码

        private boolean addIfAbsent(E e, Object[] snapshot) {
            final ReentrantLock lock = this.lock;
            lock.lock();
            try {
                Object[] current = getArray();
                int len = current.length;
                if (snapshot != current) {
                    // Optimize for lost race to another addXXX operation
                    int common = Math.min(snapshot.length, len);
                    for (int i = 0; i < common; i++)
                        if (current[i] != snapshot[i] && eq(e, current[i]))
                            return false;
                    if (indexOf(e, current, common, len) >= 0)
                            return false;
                }
                Object[] newElements = Arrays.copyOf(current, len + 1);
                newElements[len] = e;
                setArray(newElements);
                return true;
            } finally {
                lock.unlock();
            }
        }
    

    可以看到,同样使用了复制的数据进行数据的添加

    优点

    对于一些读多写少的数据,这种做法的确很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。

    缺点

    这种实现只是保证数据的最终一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据。如果对象比较大,频繁地进行替换会消耗内存,从而引发Java的GC问题,这个时候,我们应该考虑其他的容器,例如ConcurrentHashMap。

    Collections.synchronizedList

    首先来看下Collections.synchronizedList的源码:

    public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }
    

    这个方法回根据你传入的List是否实现RandomAccess这个接口来返回的SynchronizedRandomAccessList还是SynchronizedList.

    再看一下SynchronizedList的源码

    static class SynchronizedList<E>
        extends SynchronizedCollection<E>
        implements List<E> {
        private static final long serialVersionUID = -7754090372962971524L;
    
        final List<E> list;
    
        SynchronizedList(List<E> list) {
            super(list);
            this.list = list;
        }
        SynchronizedList(List<E> list, Object mutex) {
            super(list, mutex);
            this.list = list;
        }
    
        public boolean equals(Object o) {
            if (this == o)
                return true;
            synchronized (mutex) {return list.equals(o);}
        }
        public int hashCode() {
            synchronized (mutex) {return list.hashCode();}
        }
    
        public E get(int index) {
            synchronized (mutex) {return list.get(index);}
        }
        public E set(int index, E element) {
            synchronized (mutex) {return list.set(index, element);}
        }
        public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);}
        }
        public E remove(int index) {
            synchronized (mutex) {return list.remove(index);}
        }
    
        public int indexOf(Object o) {
            synchronized (mutex) {return list.indexOf(o);}
        }
        public int lastIndexOf(Object o) {
            synchronized (mutex) {return list.lastIndexOf(o);}
        }
    
        public boolean addAll(int index, Collection<? extends E> c) {
            synchronized (mutex) {return list.addAll(index, c);}
        }
    
        public ListIterator<E> listIterator() {
            return list.listIterator(); // Must be manually synched by user
        }
    
        public ListIterator<E> listIterator(int index) {
            return list.listIterator(index); // Must be manually synched by user
        }
    
        public List<E> subList(int fromIndex, int toIndex) {
            synchronized (mutex) {
                return new SynchronizedList<>(list.subList(fromIndex, toIndex),
                                            mutex);
            }
        }
    
        @Override
        public void replaceAll(UnaryOperator<E> operator) {
            synchronized (mutex) {list.replaceAll(operator);}
        }
        @Override
        public void sort(Comparator<? super E> c) {
            synchronized (mutex) {list.sort(c);}
        }
        ... ...
    }
    

    可以看到SynchronizedList类中对add,remove, get等方法都加了synchronized关键字修饰,在保证List相关机制不变的情况下,保证的线程安全

    但是可以看到 listIterator()获取迭代器的相关方法并有使用synchronized关键字,因此在进行迭代遍历的时候,需要加锁

    List<String> list = Collections.synchronizedList(new ArrayList<String>());
    list.add("1");
    list.add("2");
    list.add("3");
    
    synchronized (list) {
        Iterator i = list.iterator(); // Must be in synchronized block
        while (i.hasNext()) {
            //foo(i.next());
            System.out.println(i.next());
        }
    }
    

    几种同步List实现对比

    ArrayList

    ArrayList是非线性安全,此类的 iterator 和 listIterator 方法返回的迭代器是快速失败的:在创建迭代器之后,除非通过迭代器自身的 remove 或 add 方法从结构上对列表进行修改,否则在任何时间以任何方式对列表进行修改,迭代器都会抛出 ConcurrentModificationException。即在一方在便利列表,而另一方在修改列表时,会报ConcurrentModificationException错误。而这不是唯一的并发时容易发生的错误,在多线程进行插入操作时,由于没有进行同步操作,容易丢失数据。

    因此,在开发过程当中,ArrayList并不适用于多线程的操作。

    Vector

    从JDK1.0开始,Vector便存在JDK中,Vector是一个线程安全的列表,采用数组实现。其线程安全的实现方式是对所有操作都加上了synchronized关键字,这种方式严重影响效率,因此,不再推荐使用Vector了,Stackoverflow当中有这样的描述:Why is Java Vector class considered obsolete or deprecated?

    Collections.synchronizedList

    Collections.synchronizedList的源码可知,其实现线程安全的方式是建立了list的包装类,代码如下:

    public static <T> List<T> synchronizedList(List<T> list) {  
    return (list instanceof RandomAccess ?  
                   new SynchronizedRandomAccessList<T>(list) :  
                   new SynchronizedList<T>(list));//根据不同的list类型最终实现不同的包装类。  
       }  
    

    其中,SynchronizedList对部分操作加上了synchronized关键字以保证线程安全。部分SynchronizedList的代码如下:

    image.png

    CopyOnWriteArrayList

    从字面可以知道,CopyOnWriteArrayList在线程对其进行些操作的时候,会拷贝一个新的数组以存放新的字段。

    CopyOnWriteArrayList和Collections.synchronizedList对比

    CopyOnWriteArrayList和Collections.synchronizedList是实现线程安全的列表的两种方式。两种实现方式分别针对不同情况有不同的性能表现,其中CopyOnWriteArrayList的写操作性能较差,而多线程的读操作性能较好。而Collections.synchronizedList的写操作性能比CopyOnWriteArrayList在多线程操作的情况下要好很多,而读操作因为是采用了synchronized关键字的方式,其读操作性能并不如CopyOnWriteArrayList。因此在不同的应用场景下,应该选择不同的多线程安全实现类。


    参考:

    Collections.synchronizedList使用方法

    CopyOnWriteArrayList与Collections.synchronizedList的性能对比

    相关文章

      网友评论

          本文标题:几种Java同步List对比

          本文链接:https://www.haomeiwen.com/subject/munxkktx.html