美文网首页
多线程并发下的线程安全的集合类的使用

多线程并发下的线程安全的集合类的使用

作者: 青见青 | 来源:发表于2018-06-29 16:17 被阅读0次

    1、概念简介:

    线程安全:就是当多线程访问时,采用了加锁的机制;即当一个线程访问该类的某个数据时,会对这个数据进行保护,其他线程不能对其访问,直到该线程读取完之后,其他线程才可以使用。防止出现数据不一致或者数据被污染的情况。

    线程不安全:就是不提供数据访问时的数据保护,多个线程能够同时操作某个数据,从而出现数据不一致或者数据污染的情况。

    对于线程不安全的问题,一般会使用synchronized关键字加锁同步控制。

    线程安全 工作原理: jvm中有一个main memory对象,每一个线程也有自己的working memory,一个线程对于一个变量variable进行操作的时候, 都需要在自己的working memory里创建一个copy,操作完之后再写入main memory。 

    当多个线程操作同一个变量variable,就可能出现不可预知的结果。 

    而用synchronized的关键是建立一个监控monitor,这个monitor可以是要修改的变量,也可以是其他自己认为合适的对象(方法),然后通过给这个monitor加锁来实现线程安全,每个线程在获得这个锁之后,要执行完加载load到working memory 到 use && 指派assign 到 存储store 再到 main memory的过程。才会释放它得到的锁。这样就实现了所谓的线程安全。

    1、 线程的安全控制有三个级别

     • JVM 级别。大多数现代处理器对并发对 某一硬件级别提供支持,通常以 compare-and-swap (CAS)指令形式。CAS 是一种低级别的、细粒度的技术,它允许多个线程更新一个内存位置,同时能够检测其他线程的冲突并进行恢复。它是许多高性能并发算法的基础。在 JDK 5.0 之前,Java 语言中用于协调线程之间的访问的惟一原语是同步,同步是更重量级和粗粒度的。公开 CAS 可以开发高度可伸缩的并发 Java 类。

     • 低级实用程序类 -- 锁定和原子类。使用 CAS 作为并发原语,ReentrantLock 类提供与 synchronized 原语相同的锁定和内存语义,然而这样可以更好地控制锁定(如计时的锁定等待、锁定轮询和可中断的锁定等待)和提供更好的可伸缩性(竞争时的高性能)。大多数开发人员将不再直接使用 ReentrantLock 类,而是使用在 ReentrantLock 类上构建的高级类。

     • 高级实用程序类。这些类实现并发构建块,每个计算机科学课本中都会讲述这些类 -- 信号、互斥、闩锁、屏障、交换程序、线程池和线程安全集合类等。大部分开发人员都可以在应用程序中用这些类,来替换许多同步、 wait() 和 notify() 的使用,从而提高性能、可读性和正确性。

    2、常见的线程安全操作列举几个

    ①加锁同步synchronizedLock等

     ②wait() notify()线程调度 已实现执行的同步

    ③ThreadLocal局部变量  每一个线程都有一份数据

     ④Semaphore 信号量

     ⑤volatile 保证一个变量的线程安全


    2、常见的数据集合简介

    常见并且常用的数据集合有map,hashmap,hashSet,TreeMap, TreeSet, List, ArrayList, LinkedList, StringBuilder

    线程安全的数据集合有:Vector, HashTable, StringBuffer,  ConcurrentHashMap, Collections.synchronizedList, CopyOnWriteArrayList

    相关集合对象的比较:

    Vector、ArrayList、LinkedList, Collections.synchronizedList, CopyOnWriteArrayList: 

    1、Vector:

    Vector与ArrayList一样,也是通过数组实现的,不同的是它支持线程的同步,即某一时刻只有一个线程能够写Vector,避免多线程同时写而引起的不一致性,但实现同步需要很高的花费,因此,访问它比访问ArrayList慢。 

    2、ArrayList: 

    a. 当操作是在一列数据的后面添加数据而不是在前面或者中间,并需要随机地访问其中的元素时,使用ArrayList性能比较好。 

    b. ArrayList是最常用的List实现类,内部是通过数组实现的,它允许对元素进行快速随机访问。数组的缺点是每个元素之间不能有间隔,当数组大小不满足时需要增加存储能力,就要讲已经有数组的数据复制到新的存储空间中。当从ArrayList的中间位置插入或者删除元素时,需要对数组进行复制、移动、代价比较高。因此,它适合随机查找和遍历,不适合插入和删除。 

    3、LinkedList: 

    a. 当对一列数据的前面或者中间执行添加或者删除操作时,并且按照顺序访问其中的元素时,要使用LinkedList。 

    b. LinkedList是用链表结构存储数据的,很适合数据的动态插入和删除,随机访问和遍历速度比较慢。另外,他还提供了List接口中没有定义的方法,专门用于操作表头和表尾元素,可以当作堆栈、队列和双向队列使用。

        Vector和ArrayList在使用上非常相似,都可以用来表示一组数量可变的对象应用的集合,并且可以随机的访问其中的元素。 


    HashTable、HashMap、HashSet: 

    HashTable和HashMap采用的存储机制是一样的,不同的是: 

    1、HashMap:

    a. 采用数组方式存储key-value构成的Entry对象,无容量限制; 

    b. 基于key hash查找Entry对象存放到数组的位置,对于hash冲突采用链表的方式去解决; 

    c. 在插入元素时,可能会扩大数组的容量,在扩大容量时须要重新计算hash,并复制对象到新的数组中; 

    d. 是非线程安全的; 

    e. 遍历使用的是Iterator迭代器;

    2、HashTable: 

    a. 是线程安全的; 

    b. 无论是key还是value都不允许有null值的存在;在HashTable中调用Put方法时,如果key为null,直接抛出NullPointerException异常; 

    c. 遍历使用的是Enumeration列举;

    3、HashSet: 

    a. 基于HashMap实现,无容量限制; 

    b. 是非线程安全的; 

    c. 不保证数据的有序;

    TreeSet、TreeMap:

    TreeSet和TreeMap都是完全基于Map来实现的,并且都不支持get(index)来获取指定位置的元素,需要遍历来获取。另外,TreeSet还提供了一些排序方面的支持,例如传入Comparator实现、descendingSet以及descendingIterator等。 

    1、TreeSet: 

    a. 基于TreeMap实现的,支持排序; 

    b. 是非线程安全的;

    2、TreeMap: 

    a. 典型的基于红黑树的Map实现,因此它要求一定要有key比较的方法,要么传入Comparator比较器实现,要么key对象实现Comparator接口; 

    b. 是非线程安全的;

    StringBuffer和StringBulider: 

    StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串。

        1、在执行速度方面的比较:StringBuilder > StringBuffer ; 

        2、他们都是字符串变量,是可改变的对象,每当我们用它们对字符串做操作时,实际上是在一个对象上操作的,不像String一样创建一些对象进行操作,所以速度快; 

        3、 StringBuilder:线程非安全的; 

        4、StringBuffer:线程安全的;

    对于String、StringBuffer和StringBulider三者使用的总结:

       1.如果要操作少量的数据用 = String 

       2.单线程操作字符串缓冲区 下操作大量数据 = StringBuilder 

       3.多线程操作字符串缓冲区 下操作大量数据 = StringBuffer

    vector,hashtable是在Java1.0就引入的集合,两个都是线程安全的,但是现在已很少使用,原因就是内部实现的线程安全太消耗资源,因此新的安全的高效的数据集合出现了,这就是ConcurrentHashMap, Collections.synchronizedList, CopyOnWriteArrayList。

    为什么我们想要新的线程安全的List类?为什么会出现CopyOnWriteArrayList?

    简单的答案是与迭代和并发修改之间的交互有关。使用 Vector 或使用同步的 List 封装器,返回的迭代器是 fail-fast 的,

    这意味着如果在迭代过程中任何其他线程修改 List,迭代可能失败。Vector 的非常普遍的应用程序是存储通过组件注册的监听器的列表。当发生适合的事件时,该组件将在监听器的列表中迭代,调用每个监听器。

    为了防止ConcurrentModificationException,迭代线程必须复制列表或锁定列表,一遍进行整体迭代,而这两种情况都需要大量的性能成本。CopyOnWriteArrayList类通过每次添加或删除元素时创建支持数组的新副本,避免了这个问题,但是进行中的迭代保持对创建迭代器时的当前副本进行操作。虽然复制也会有一些成本,但是在许多情况下,迭代要比修改多得多,在这些情况系,写入时复制要比其他备用方法具有更好的性能和并发性。

    CopyOnWriteArrayList如何做到线程安全的?

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

    当有新元素加入的时候,如下图,创建新数组,并往新数组中加入一个新元素,这个时候,array这个引用仍然是指向原数组的。

    当元素在新数组添加成功后,将array这个引用指向新数组。

    CopyOnWriteArrayList的整个add操作都是在的保护下进行的。 

    这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。

    CopyOnWriteArrayList的add操作的源代码如下:

    public boolean add(E e) {

        //1、先加锁    final ReentrantLock lock = this.lock;

        lock.lock();

        try {

            Object[] elements = getArray();

            int len = elements.length;

            //2、拷贝数组        Object[] newElements = Arrays.copyOf(elements, len + 1);

            //3、将元素加入到新数组中        newElements[len] = e;

            //4、将array引用指向到新数组        setArray(newElements);

            return true;

        } finally {

          //5、解锁        lock.unlock();

        }

    }

    由于所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况: 

    1、如果写操作未完成,那么直接读取原数组的数据; 

    2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据; 

    3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。

    可见,CopyOnWriteArrayList的读操作是可以不用加锁的。

    CopyOnWriteArrayList的使用场景

    通过上面的分析,CopyOnWriteArrayList 有几个缺点: 

    1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc

    2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;

    CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用 

    因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

    CopyOnWriteArrayList透露的思想

    如上面的分析CopyOnWriteArrayList表达的一些思想: 

    1、读写分离,读和写分开 

    2、最终一致性 

    3、使用另外开辟空间的思路,来解决并发冲突

    Collections.synchronizedList

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

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

    public static  List synchronizedList(List list) {  

    return (list instanceof RandomAccess ?  

    new SynchronizedRandomAccessList(list) :  

    new SynchronizedList(list));//根据不同的list类型最终实现不同的包装类。  

       } 

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

    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 ListIterator listIterator() {  

    return list.listIterator(); // Must be manually synched by user 需要用户保证同步,否则仍然可能抛出ConcurrentModificationException  

            }  

    public ListIterator listIterator(int index) {  

    return list.listIterator(index); // Must be manually synched by user 需要用户保证同步,否则仍然可能抛出ConcurrentModificationException  

            }

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

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


    转载自线程并发线程安全介绍及java.util.concurrent包下类介绍 - CSDN博客,

    Java中各种集合(字符串类)的线程安全性!!! - 鸿燕藏锋 - 博客园,

    线程安全的CopyOnWriteArrayList介绍 - CSDN博客,

    CopyOnWriteArrayList与Collections.synchronizedList的性能对比 - CSDN博客

    相关文章

      网友评论

          本文标题:多线程并发下的线程安全的集合类的使用

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