学习互联网架构第十课(并发类容器)

来源:互联网 发布:中美进出口数据 编辑:程序博客网 时间:2024/06/06 05:58
        并发类容器是专门针对并发设计的,使用ConcurrentHashMap来代替给予散列的传统的HashTable,而且在ConcurrentHashMap中,添加了一些常见复合操作的支持。以及使用了CopyOnWriteArrayList代替Vector,并发的CopyOnWriteArraySet,以及并发的Queue,ConcurrentLinkedQueue和LinkedBlockingQueue,前者是高性能的队列,后者是以阻塞形式的队列,具体实现Queue还有很多,例如ArrayBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。
    下面我们便一一学习下这些并发类容器。
    ConcurrentMap接口有两个重要的实现:ConcurrentHashMap和ConcurrentSkipListMap(支持并发排序功能,弥补ConcurrentHashMap)

    对同步类容器来说比如Vector和HashTable,当有多个线程都要修改容器中的元素的话只能有一个线程进入到容器内进行修改,其它线程都要在外面排队等候,如下图所示。


       ConcurrentHashMap之所以是并发容器是因为它内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的HashTable,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。把一个整体分成了16个段(Segment)。也就是最高支持16个线程的并发修改操作。这也是在多线程场景时减小锁的粒度从而降低锁竞争的一种方案。并且代码中大多共享变量使用volatile关键字声明,目的是第一时间获取修改的内容,性能非常好。
       我们还是以图来说明,在下图中,我们看到ConcurrentHashMap有16个段,你可以把每个段都相当于一个小的HashTable,也就是说每个小段都是加锁的,并发类容器只是把锁的粒度变小了,原来是一把大锁,现在分成了16个小锁。这样可以有效缓解锁竞争的问题,尽量减少因多个线程争抢一把锁而导致的CPU瞬间爆满的情况。现在有5个线程要修改容器中的元素,假如一、二、三、四4个线程访问的是不同的段,那么它们都可以直接访问它们各自段内的数据并且做修改操作,但是如果几个线程访问同一个小段的话(比如线程四和线程五),那么只能进入一个线程进行修改操作,其它线程只能在外等候。也就是说并发类容器虽然支持并发操作,但是如果并发量非常高的话,也是需要耗费很多时间的,毕竟容器内部只有16个分段,一次只能同时处理16个请求,其余的就要排队等候。不过这相比于同步类容器来说,性能已经好太多了。


          下面我们简单看个ConcurrentHashMap的例子吧,可以看到使用chm.putIfAbsent("k3", "vvvv");方法的话,会先判断map中是否已经添加过名为"k3"的key了。如果已经添加过了,那么就不再添加了。如果没有添加过的话,就会添加。


         将chm.putIfAbsent("k3", "vvvv");中的"k3"换成"k4",这时再执行main方法,如下图所示。


        下面我们来学习下一个并发类容器:Copy-On-Write容器,Copy-On-Write简称COW,是一种用于程序设计中的优化策略。JDK里的COW容器有两种,分别是
CopyOnWriteArrayList和CopyOnWriteArraySet,COW容器非常有用,可以在非常多的场景中使用到。
         那么什么是CopyOnWrite容器呢?CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往容器添加,而是先将当前容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写在不同的容器。
        光说理论不容易理解,还是上图吧,在下图中,当线程一想要对CopyOnWRite容器进行写操作时,CopyOnWrite容器会复制一份容器出来,然后让线程一去修改容器的副本,而不是修改原容器,这时指针依然指在原容器上,写的过程中如果有多个线程想要读取容器中的数据,那么这些线程会去指针指向的容器中去读取数据。不用加任何锁,这样无论有多少个线程同时访问原容器都是可以的,效率当然就很高了。但是肯定有人会有疑问,就是如果同一时间有多个请求过来要修改容器内的数据,会怎么样?这种情况下,当第一个写请求被容器收到后,容器复制出一个副本出来,线程一去这个副本修改数据,其它线程也要修改容器数据,那就只能在副本容器外排队等候,因为容器的写操作是加锁的。


         当写操作完成后,指针会指向容器的副本,原容器将会被销毁,被垃圾回收器回收。后续再有线程来请求读取容器中的数据的话,就会读取副本这个容器中的数据了。


          下面我们来看下CopyOnWriteArrayList和CopyOnWriteArraySet的一个小例子,如下图所示,可以看到CopyOnWriteArrayList与ArrayList的用法是一样的,
CopyOnWriteArraySet与Set的用法也是一样的。


         代码如下:

package com.internet.container;import java.util.Iterator;import java.util.concurrent.CopyOnWriteArrayList;import java.util.concurrent.CopyOnWriteArraySet;public class UseCopyOnWrite {   public static void main(String[] args) {//ArrayList怎样使用,CopyOnWriteArrayList就怎样使用CopyOnWriteArrayList<String> cwal = new CopyOnWriteArrayList<>();cwal.add("a");cwal.add("b");        for(String str : cwal){System.out.println("str:"+str);}        //Set怎样使用,CopyOnWriteArraySet就怎样使用CopyOnWriteArraySet<String> cwas = new CopyOnWriteArraySet<>();cwas.add("c");cwas.add("d");for (Iterator iterator = cwas.iterator(); iterator.hasNext();) {String string = (String) iterator.next();System.out.println("string:"+string);}}}
        两种容器add方法都添加了重入锁,如下图所示

          CopyOnWriteArraySet.class类中的代码如下:

         那么CopyOnWrite容器的应用场景是什么呢?
        CopyOnWrite容器特别适用于读多写少的场景,就是基本上都是在读,很少有写的请求的场景。如果是写多读少的话,就不适合,因为CopyOnWrite处理写请求是要复制一份副本出来的,如果容器内的数据量非常大的话,频繁的复制副本将会非常消耗性能,这种场景下CopyOnWrite容器就根本不适用,它甚至没有你在写操作的地方加一把synchronized锁的性能高。所以我们应用CopyOnWrite容器的时候一定要注意使用场景。   

阅读全文
0 0
原创粉丝点击