• 并发容器(抄袭篇)




    说明:等于转载的博客,出自JavaGuide(还是有自己手动实践一遍的)


    1. 概览

    JDK提供的并发容器在java.util.concurrent中,即出名的JUC,主要有:

    • ConcurrentHashMap:线程安全的HashMap
    • CopyOnWriteArrayList:线程安全的list
    • ConcurrentLinkedQueue:高效的并发队列,和queue,stack比较
    • BlockingQueue:是接口:通过链表,数组等方式实现,表示阻塞队列,适用于数据共享通道
    • ConcurrentSkipListMap:跳表的实现,这是一个 Map,使用跳表的数据结构进行快速查找




    2. ConcurrentHashMap

    采用CAS和synchronized保证并发安全,不同的是写的时候只锁定首节点,只要不发生碰撞,就不会并发,相比以前的分段提高了并发能力。底层使用:Node数组+链表/红黑树





    3. CopyOnWriteArrayList

    日常中我们很多都是读操作占了一大部分,反而写操作比较少。如果每次都对读操作进行加锁是一种浪费,所以我们允许多线程并发访问读取数据,而且写入也不会阻塞读取操作,只有写入和写入需要进行同步等待。


    CopyOnWrite :在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了


    CopyOnWriteArrayList类的所有可变操作(add,set 等等)都是通过创建底层数组的新副本来实现的。看名字就知道:写入的时候复制一份。当 List 需要被修改的时候,我并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了


    比如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();
    	}
    }
    

    注意:复制的时候是需要加锁的,避免多线程复制多份出来





    Java 提供的线程安全的 Queue 可以分为阻塞队列非阻塞队列,其中阻塞队列的典型例子是 BlockingQueue,非阻塞队列的典型例子是 ConcurrentLinkedQueue,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 阻塞队列可以通过加锁来实现,非阻塞队列可以通过 CAS 操作实现。

    4. ConcurrentLinkedQueue

    其内部使用了CAS来实现,适用于多线程访问的队列,且加锁成本过高的时候。至于CAS属于并发锁的内容,后面等笔者复习到的时候就写博客来介绍一下





    上面我们己经提到了 ConcurrentLinkedQueue 作为高性能的非阻塞队列。下面我们要讲到的是阻塞队列——BlockingQueue。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是 BlockingQueue 提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止

    5. BlockingQueue

    BlockingQueue 是一个接口,继承自 Queue,所以其实现类也可以作为 Queue 的实现来使用,而 Queue 又继承自 Collection 接口。下面是 BlockingQueue 的相关实现类:

    • ArrayBlockingQueue
    • LinkedBlockingQueue
    • PriorityBlockingQueue

    5.2 ArrayBlockingQueue

    ArrayBlockingQueue 是 BlockingQueue 接口的有界队列实现类,底层采用数组来实现。ArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁来控制,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞

    ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在,当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue,可采用如下代码:

    private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);
    

    5.3 LinkedBlockingQueue

    LinkedBlockingQueue 底层基于单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE


    5.4 PriorityBlockingQueue

    PriorityBlockingQueue 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则。

    PriorityBlockingQueue 并发控制采用的是 ReentrantLock,队列为无界队列(ArrayBlockingQueue 是有界队列,LinkedBlockingQueue 也可以通过在构造函数中传入 capacity 指定队列最大的容量,但是 PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会自动扩容)。

    简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。





    6. ConcurrentSkipListMap

    为了引出 ConcurrentSkipListMap,先带着大家简单理解一下跳表。

    对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是 O(logn) 所以在并发数据结构中,JDK 使用跳表来实现一个 Map


    跳表的本质是同时维护了多个链表,并且链表是分层的,

    最低层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层的子集。

    跳表内的所有链表的元素都是排序的。查找时,可以从顶级链表开始找。一旦发现被查找的元素大于当前链表中的取值,就会转入下一层链表继续找。这也就是说在查找过程中,搜索是跳跃式的。如上图所示,在跳表中查找元素 18。

    查找 18 的时候原来需要遍历 18 次,现在只需要 7 次即可。针对链表长度比较大的时候,构建索引查找效率的提升就会非常明显。

    从上面很容易看出,跳表是一种利用空间换时间的算法。

    使用跳表实现 Map 和使用哈希算法实现 Map 的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择。JDK 中实现这一数据结构的类是 ConcurrentSkipListMap。



  • 相关阅读:
    在禅道中实现WORD等OFFICE文档转换为PDF进行在线浏览
    慎用 supportedRuntime
    定时开关机方案
    谨慎使用Sql server data tool 架构比对排除
    生成数据字典
    Cordova开发速记
    SQLSERVER 2012 收缩日志
    QService 服务容器
    使用用户自定义类型 CLR UDT
    使用DOTNETZIP过滤并压缩相对目录
  • 原文地址:https://www.cnblogs.com/Howlet/p/12517439.html
Copyright © 2020-2023  润新知