zoukankan      html  css  js  c++  java
  • 浅析Collections.synchronizedList实现原理及如何做到线程安全、实现线程安全2种方式CopyOnWriteArrayList与Collections.synchronizedList的读写性能对比

    一、Collections.synchronizedList 实现原理及如何做到线程安全

      大家都知道ArrayList并不是线程安全的,如果想要做到线程安全,我们可以使用 Collections.synchronizedList, 但是使用 Collections.synchronizedList后是否真的就线程安全了?

    1、Collections.synchronizedList 原理

      我们先来看看Collections.synchronizedList 做了什么。

        从源码来看,SynchronizedList 就是在 List的操作外包加了一层 synchronize 同步控制。

    2、加了 Collections.synchronizedList 后,为什么还需要使用 synchronized ?

    首先我们看官方文档,可以发现, 当用户通过迭代器遍历返回列表时,必须手动同步:

    It is imperative that the user manually synchronize on the returned list when traversing it via [Iterator]

    List list = Collections.synchronizedList(new ArrayList());
          ...
      synchronized (list) {
          Iterator i = list.iterator(); // Must be in synchronized block
          while (i.hasNext())
              foo(i.next());
      }

      也就是说官方文档明确提出:对于 使用 Iterator 遍历列表时,Collections.synchronizedList 可能发生错误。

      那除了直接使用 Iterator 要加 synchronize 保证线程安全,还有什么情况会间接使用到 Iterator吗? 那就是 for each增强for循环;

      在使用 Iteratior 遍历的同时,异步修改List的结构,发现抛出了 ConcurrentModificationException 异常;

      那怎么解决呢?官方文档说的很清楚,我们在迭代器遍历返回列表时,增加手动同步处理,下面是IteratorRunnable 修改后 代码,仅仅是在外层加了 synchronized.

        static class IteratorRunnable implements Runnable {
           private List<Integer> list;
           public IteratorRunnable(List<Integer> synchronizeList) {
               this.list = synchronizeList;
           }
           @Override
           public void run() {
               while(true) {
                   synchronized (list) {
                       for (Integer i : list) {
                           try {
                               TimeUnit.SECONDS.sleep(1);
                           } catch (InterruptedException e) {
                               e.printStackTrace();
                           }
                           System.out.println(i + ",");
                       }
                   }
               }
           }
       }

      从运行结果来看,增加了synchronized 后,不会出现ConcurrentModificationException异常了。

    3、探究下for each Java的实现

      我们先来看看.class文件中for each

      看到这,我们就可以确定 ,其实JAVA中的增强for循环底层是通过iterator来实现的。

    二、CopyOnWriteArrayList与Collections.synchronizedList的性能对比

      列表实现有 ArrayList、Vector、CopyOnWriteArrayList、Collections.synchronizedList(list) 四种方式。

    1、ArrayList

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

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

    2、Vector

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

    3、Collections.synchronizedList & CopyOnWriteArrayList

      CopyOnWriteArrayList 和 Collections.synchronizedList 是实现线程安全的列表的两种方式。

      两种实现方式分别针对不同情况有不同的性能表现,其中 CopyOnWriteArrayList的写操作性能较差,而多线程的读操作性能较好。

      而Collections.synchronizedList的写操作性能比CopyOnWriteArrayList在多线程操作的情况下要好很多,而读操作因为是采用了synchronized关键字的方式,其读操作性能并不如CopyOnWriteArrayList。

      因此在不同的应用场景下,应该选择不同的多线程安全实现类。

    4、Collections.synchronizedList

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

      其中,SynchronizedList对部分操作加上了synchronized关键字以保证线程安全。但其iterator()操作还不是线程安全的。

    5、CopyOnWriteArrayList

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

      其没有加任何同步关键字,根据以上写操作的代码可知,其每次写操作都会进行一次数组复制操作,然后对新复制的数组进行些操作,不可能存在在同时又读写操作在同一个数组上( 不是同一个对象),而读操作并没有对数组修改,不会产生线程安全问题。

      Java中两个不同的引用指向同一个对象,当第一个引用指向另外一个对象时,第二个引用还将保持原来的对象。

      其中setArray()操作仅仅是对array进行引用赋值。Java中“=”操作只是将引用和某个对象关联,假如同时有一个线程将引用指向另外一个对象,一个线程获取这个引用指向的对象,那么他们之间不会发生ConcurrentModificationException,他们是在虚拟机层面阻塞的,而且速度非常快,是一个原子操作,几乎不需要CPU时间。

      在列表有更新时直接将原有的列表复制一份,并再新的列表上进行更新操作,完成后再将引用移到新的列表上。旧列表如果仍在使用中(比如遍历)则继续有效。如此一来就不会出现修改了正在使用的对象的情况(读和写分别发生在两个对象上),同时读操作也不必等待写操作的完成,免去了锁的使用加快了读取速度。

    6、Collections.synchronizedList & CopyOnWriteArrayList在读写操作上的差距

    (1)写操作:在线程数目增加时CopyOnWriteArrayList的写操作性能下降非常严重,而Collections.synchronizedList虽然有性能的降低,但下降并不明显。

    (2)读操作:在多线程进行读时,Collections.synchronizedList和CopyOnWriteArrayList均有性能的降低,但是Collections.synchronizedList的性能降低更加显著。

      结论: 

      CopyOnWriteArrayList,发生修改时候做 copy,新老版本分离,保证读的高性能,适用于以读为主,读操作远远大于写操作的场景中使用,比如缓存。

      而Collections.synchronizedList则可以用在CopyOnWriteArrayList不适用,但是有需要同步列表的地方, 读写操作都比较均匀的地方。

  • 相关阅读:
    LeetCode 189. Rotate Array
    LeetCode 965. Univalued Binary Tree
    LeetCode 111. Minimum Depth of Binary Tree
    LeetCode 104. Maximum Depth of Binary Tree
    Windows下MySQL的安装与配置
    LeetCode 58. Length of Last Word
    LeetCode 41. First Missing Positive
    LeetCode 283. Move Zeroes
    《蚂蚁金服11.11:支付宝和蚂蚁花呗的技术架构及实践》读后感
    删除docker下的镜像
  • 原文地址:https://www.cnblogs.com/goloving/p/15248448.html
Copyright © 2011-2022 走看看