ConcurrentSkipListMap和ConcurrentSkipListSet
ConcurrentSkipListMap和ConcurrentSkipListSet分别是TreeMap和TreeSet有序的容器的并发版本,内部加入了跳表。
1,为什么选择跳表?
目前经常使用的平衡数据结构有:B树,红黑树,AVL树,SplayTree,Treep等,想象一下,给你一张草稿纸,一只笔,一个编辑器,你能立即实现一颗红黑树,或者AVL树出来吗?很难吧,这需要时间,要考虑很多细节,要参考一堆算法与数据结构之类的树,还要参考网上的代码,相当麻烦。
用跳表吧,跳表是一种随机化的数据结构,目前开源软件Redis和LevelDB(全文搜索引擎)都有用到它,它的效率和红黑树以及AVL树不相上下,但跳表的原理相当简单,时间复杂度非常趋近于红黑树。
跳表:
跳表(可以提高普通链表查询的速度,是扩展空间负责度):
SkipList,以空间换时间,在原链表的基础上形成多层索引,当某个节点在插入时,随机决定这个节点是否成为上层索引,所以跳表又称为概率数据结构,这样有了索引之后,在查找的时候提高了效率。
跳表的高度:
n个元素的跳表,每个元素插入的时候都要做一次实验,用来决定元素占据的层数K,跳表的高度等于这n次实验中产生的最大k
跳表具有如下性质:
(1) 由很多层结构组成。
(2) 每一层都是一个有序的链表。
(3) 最底层(Level 1)的链表包含所有元素。
(4) 如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现。
(5) 每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素。
2,ConcurrentHashMap中为什么不使用跳表?
因为ConcurrentHashMap,因为用了分段锁,本身的空间利用率很低,如果再使用跳表用空间换取时间,就会使得ConcurrentHashMap的空间利用率更低,所以不使用跳表。
ConcurrentLinkedQueue
ConcurrentLinkedQueue是LinkedList的并发版本,无界非阻塞队列,底层是个链表,遵循先进先出FIFO原则。
其中的方法:
add,offer:将元素插入到尾部
peek:拿头部的数据,但是不移除
poll:拿头部的数据,但是移除
写时复制容器
写时复制容器有两个,是CopeOnWriteArrayList和CopeOnWriteArraySet
写时复制的容器,通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以写时复制容器也是一种读写分离的思想,读和写不同的容器。如果读的时候有多个线程正在向容器添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的,只能保证最终一致性。
适用读多写少的并发场景,常见应用:白名单/黑名单,商品类目的访问和更新场景。
注意:存在内存占用问题。
和读写锁的区别?
写时复制容器是读写可以同时进行的,而读写锁是一个线程写的时候,其他线程不能读,是互斥的。