17 识别不同场景下的最优容器

201 阅读5分钟

大家好,我是小水珠。

今天我要和你分享的话题就是:在不同场景下我们该如何选择最优容器。

一 并发场景下的Map容器

**假设我们现在要给一个电商系统设计一个简单的统计商品销量TOP 10的功能。常规情况下,我们是用一个哈希表来储存商品和销量键值对,然后使用排序获得销量前10的商品。在这里,哈希表是实现该功能的关键。那么请思考一下,如果你设计这个功能,你会使用哪个容器呢? **

1.HashTable对比ConcurrentHashMap

我们可以进一步对比看看以上两种容器。

在数据不断的写入和删除,且不存在数据量累积以及数据排序的场景下,我们可以选用HashTable或ConcurrentHashMap。

HashTable使用Sychronized同步锁修饰了put,get,remove等方法,因此在高并发场景下,读写操作都会存在大量锁竞争,给系统带来性能开销。

相比HashTable,ConcurrentHashMap在保证线程安全的基础上兼具了更好的并发性能。在JDK1.7中,ConcurrentHashMap就使用了分段锁Segment减小了锁粒度,最终优化了锁的并发操作。

17-链表锁.jpg

到了JDK1.8,ConcurrentHashMap做了大量的改动,摒弃了Segment的概念,由于Sychronized锁在Java6之后的性能已经得到了很大的提升,所以在JDK1.8中,Java重新启用了Sychronized同步锁,通过Sychronized实现HashEntry作为锁粒度。这种改动将数据结构变得更加简单了,操作也更加清晰流畅。

与JDK1.7的put方法一样,JDK1.8在添加元素时,在没有哈希冲突的情况下,会使用CAS进行添加元素操作;如果有冲突,则通过Sychronized将链表锁定,在执行接下来的操作。

2.ConcurrentHashMap对比ConcurrentSkipListMap

我们再看一个案例,我上家公司的操作系统中有这样一个功能,提醒用户手机卡实时流量不足。主要的流程是服务端先通过虚拟运营商同步用户实时流量,再通过手机端定时触发查询功能,如果流量不足,就弹出系统通知。

该功能的特点是用户量大,并发量高,写入多于查询操作,这时候我们就需要设计一个缓存,用来存放这些用户以及对应的流量键值对信息。那么假设让你设计一个简单的缓存,你会怎么设计呢?

你可能会考虑使用ConcurrentHashMap容器,但我在07讲中说过,该容器在数据量比较大的时候,链表会转换为红黑树。红黑树在并发情况下,删除和插入过程中有个平衡的过程,会牵涉到大量的节点,因此竞争锁资源的代价相对比较高。

而跳跃表的操作针对局部,需要锁住的节点少,因此在并发场景下的性能会更好一些。你可能会问了,在非线程安全的Map容器中,我并没有看到基于跳跃表实现的SkipListMap呀?这是因为在非线程安全的Map容器中,基于红黑树实现的TreeMap在单线程中的性能表现的并不比跳跃表差。

3.什么是跳跃表

跳跃表是基于链表扩展实现的一种特殊链表,类似于树的实现,跳跃表不仅实现了横向链表,还实现了垂直方向的分层索引。

首先是一个初始化的跳跃表:

17-初始化跳跃表.jpg

当查询key值为9的节点时,此时查询路径为:

17-查询9.jpg

**当新增一个key值为8的节点时,首先新增一个节点到最底层的链表中,根据概率算出level值,再根据level值新建索引层,最后链接索引层的新节点。新增节点和链接索引都是基于CAS操作实现的。 **

17-新增8.jpg

当删除一个key值为7的时,首先找到待删除节点,将其value值设置为null;之后再向待删除节点的next位置新增一个标记节点,以便减少并发冲突;然后让待删除节点的前驱节点直接越过本身指向的待删节点,直接指向后继节点,中间要被删除的节点最终将会被JVM垃圾回收处理掉;最后判断此次删除后是否导致某一索引层没有其它节点了,并视情况删除该索引层。

17-删除7.jpg

二 并发场景下的List容器

下面我们再来看一个实际生产环境中的案例。在大部分互联网产品中,都会设置一份黑名单。例如,在电商系统中,系统可能会将一些频繁参与抢购却放弃付款的用户放入黑名单列表。想想这个时候你又会使用哪个容器呢?

我讲过ArrayList是非线程安全容器,在并发场景下使用很可能会导致线程安全问题。这时,我们就可以考虑使用Java在并发编程中提供的线程安全数据,包括Vector和CopyOnWriteArrayList。

Vector也是基于Sychronized同步锁实现的线程安全,Sychronized关键字几乎修饰了所有对外暴露的方法,所以在读远大于写的造作场景中,Vector将发生大量锁竞争,从而给系统带来性能开销。

相比之下,CopyOnWriteArrayList是java.util.concurrent包提供的方法,它实现了读操作无锁,写操作则通过底层数组的新副本来实现,是一种读写分离的并发策略。我们可以通过以下图示来了解下CopyOnWriteArrayList的具体实现原理。

17-读写锁.jpg

三 总结

在并发编程中,我们经常会使用容器来存储数据或对象。Java在JDK1.1到1.8这个漫长的发展过程中,依据场景的变化实现了同类型的多种容器。我将今天的主要内容为你总结了一张表格,希望能对你有所帮助。

17-不同类型容器对比.jpg