3.实战java高并发程序设计--JDK并发包---3.3不要重复发明轮子:JDK的并发容器(工具类)

除了提供诸如同步控制、线程池等基本工具外,为了提高开发人员的效率,JDK还为大家准备了一大批好用的容器类,可以大大减少开发工作量。大家应该都听说过一种说法,所谓程序就是“算法+数据结构”,这些容器类就是为大家准备好的线程数据结构。你可以在里面找到链表、HashMap、队列等。当然,它们都是线程安全的。

在这里,我也打算花一些篇幅为大家介绍一下这些工具类。这些容器类的封装都是非常完善并且“平易近人”的,也就是说只要你有那么一点点的编程经验,就可以非常容易地使用这些容器。因此,我可能会花更多的时间来分析这些工具的具体实现,希望起到抛砖引玉的作用。

3.3.1 超好用的工具类:并发集合简介

JDK提供的这些容器大部分在java.util.concurrent包中。我先提纲挈领地介绍一下它们,初次露脸,大家只需要知道它们的作用即可。有关具体的实现和注意事项,在后面我会一一道来。

● ConcurrentHashMap:这是一个高效的并发HashMap。你可以把它理解为一个线程安全的HashMap。

● CopyOnWriteArrayList:这是一个List,从名字看就知道它和ArrayList是一族的。在读多写少的场合,这个List的性能非常好,远远优于Vector。

● ConcurrentLinkedQueue:高效的并发队列,使用链表实现。可以看作一个线程安全的LinkedList。

● BlockingQueue:这是一个接口,JDK内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合作为数据共享的通道。

● ConcurrentSkipListMap:跳表的实现。这是一个Map,使用跳表的数据结构进行快速查找。除以上并发包中的专有数据结构以外,java.util下的Vector是线程安全的(虽然性能和上述专用工具没得比),另外Collections工具类可以帮助我们将任意集合包装成线程安全的集合
3.3.2 线程安全的HashMap

在之前的章节中,已经给大家展示了在多线程环境中使用HashMap所带来的问题,如果需要一个线程安全的HashMap应该怎么做呢?一种可行的方法是使用Collections.synchronizedMap()方法包装我们的HashMap。如下代码,产生的HashMap就是线程安全的。

Collections.synchronizedMap()方法会生成一个名为SynchronizedMap的Map。它使用委托,将自己所有Map相关的功能交给传入的HashMap实现,而自己则主要负责保证线程安全。

具体参考下面的实现,首先SynchronizedMap内包装了一个Map。在内部加锁实现互斥操作,

虽然这个包装的Map可以满足线程安全的要求,但是它在多线程环境中的性能表现并不算太好。无论是对Map的读取或者写入,都需要获得mutex的锁,这会导致所有对Map的操作全部进入等待状态,直到mutex锁可用。如果并发级别不高,那么一般也够用。但是,在高并发环境中,我们有必要寻求新的解决方案

一个更加专业的并发HashMap是ConcurrentHashMap,它位于java.util.concurrent包内,专门为并发进行了性能优化,因此更适合多线程的场合。

3.3.3 有关List的线程安全

队列、链表之类的数据结构也是极其常用的,几乎所有的应用程序都会与之相关。在Java中,ArrayList和Vector都使用数组作为其内部实现。两者最大的不同在于Vector是线程安全的,而ArrayList不是。此外,LinkedList使用链表的数据结构实现了List。但是很不幸,LinkedList并不是线程安全的,不过参考前面对HashMap的包装,这里我们也可以使用Collections.synchronizedList()方法来包装任意List:

3.3.4 高效读写的队列:深度剖析ConcurrentLinkedQueue类

队列Queue也是常用的数据结构之一。在JDK中提供了一个ConcurrentLinkedQueue类用来实现高并发的队列。从名字可以看到,这个队列使用链表作为其数据结构。有关ConcurrentLinkedQueue类的性能测试大家可以自行尝试,这里限于篇幅就不再给出性能测试的代码了。大家只要知道ConcurrentLinkedQueue类应该算是在高并发环境中性能最好的队列就可以了。它之所以能有很好的性能,是因为其内部复杂的实现。

3.3.5 高效读取:不变模式下的CopyOnWriteArrayList类

在很多应用场景中,读操作可能会远远大于写操作。比如,有些系统级别的信息,往往只需要加载或者修改很少的次数,但是会被系统内所有模块频繁访问。对于这种场景,我们最希望看到的就是读操作可以尽可能地快,而写即使慢一些也没有太大关

由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问List的内部数据,毕竟读取操作是安全的。根据读写锁的思想,读锁和读锁之间确实也不冲突。但是,读操作会受到写操作的阻碍,当写发生时,读就必须等待,否则可能读到不一致的数据。同理,当读操作正在进行时,程序也不能进行写入。

从这个类的名字我们可以看到,所谓CopyOnWrite就是在写入操作时,进行一次自我复制。换句话说,当这个List需要修改时,我并不修改原有的内容(这对于保证当前在读线程的数据一致性非常重要),而是对原有的数据进行一次复制,将修改的内容写入副本中。写完之后,再用修改完的副本替换原来的数据,这样就可以保证写操作不会影响读了。需要注意的是:读取代码没有任何同步控制和锁操作,理由就是内部数组array不会发生修改,只会被另外一个array替换,因此可以保证数据安全。

3.3.6 数据共享通道:BlockingQueue

前面我们已经提到了ConcurrentLinkedQueue类是高性能的队列。对于并发程序而言,高性能自然是一个我们需要追求的目标,但多线程的开发模式还会引入一个问题,那就是如何进行多个线程间的数据共享呢?比如,线程A希望给线程B发一条消息,用什么方式告知线程B是比较合理的呢?一般来说,我们总是希望整个系统是松散耦合的。比如,你所在小区的物业希望可以得到一些业主的意见,设立了一个意见箱,如果对物业有任何要求或者意见都可以投到意见箱里。作为业主的你并不需要直接找到物业相关的工作人员就能表达意见。实际上,物业的工作人员也可能经常发生变动,直接找工作人员未必是一件方便的事情。而你投递到意见箱的意见总是会被物业的工作人员看到,不管是否发生了人员的变动。这样你就可以很容易地表达自己的诉求了。你既不需要直接和他们对话,又可以轻松提出自己的建议(这里假定物业公司的员工都是尽心尽责的好员工)。

将这个模式映射到我们程序中,就是说我们既希望线程A能够通知线程B,又希望线程A不知道线程B的存在。

这样,如果将来进行重构或者升级,我们完全可以不修改线程A,而直接把线程B升级为线程C,保证系统的平滑过渡。而这中间的“意见箱”就可以使用BlockingQueue来实现。'

与之前提到的ConcurrentLinkedQueue类或者CopyOnWriteArrayList类不同,BlockingQueue是一个接口,并非一个具体的实现。它的主要实现有下面一些,如图3.17所示。

而BlockingQueue之所以适合作为数据共享的通道,其关键还在于Blocking上。Blocking是阻塞的意思,当服务线程(服务线程指不断获取队列中的消息,进行处理的线程)处理完成队列中所有的消息后,它如何知道下一条消息何时到来呢?一种最简单的做法是让这个线程按照一定的时间间隔不停地循环和监控这个队列

一种最简单的做法是让这个线程按照一定的时间间隔不停地循环和监控这个队列。这是一种可行的方案,但显然造成了不必要的资源浪费,而且循环周期也难以确定。BlockingQueue很好地解决了这个问题。它会让服务线程在队列为空时进行等待,当有新的消息进入队列后,自动将线程唤醒,如图3.18所示。那它是如何实现的呢?我们以ArrayBlockingQueue类为例,来一探究竟。

3.3.7 随机数据结构:跳表(SkipList)

在JDK的并发包中,除常用的哈希表外,还实现了一种有趣的数据结构—跳表。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整,而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。就查询的性能而言,因为跳表的时间复杂度是O(logn),所以在并发数据结构中,JDK使用跳表来实现一个Map。

跳表的另外一个特点是随机算法。跳表的本质是同时维护了多个链表,并且链表是分层的。图3.19是跳表结构示意图。

底层的链表维护了跳表内所有的元素,每上面一层链表都是下面一层链表的子集,一个元素插入哪些层是完全随机的。因此,如果运气不好,你可能会得到一个性能很糟糕的结构。但是在实际工作中,它的表现是非常好的。

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

实现这一数据结构的类是ConcurrentSkipListMap。下面展示了跳表的简单使用方法

和HashMap不同,对跳表的遍历输出是有序的


发布了24 篇原创文章 · 获赞 1 · 访问量 3413

猜你喜欢

转载自blog.csdn.net/ashylya/article/details/104342987
今日推荐