一、JDK的并发容器
一、JUC中提供的并发容器
1、线程安全的HashMap
1)、使用Collections.synchronizedMap()方法,此时是使用synchronized关键字来保证线程安全。
2)、更加高效的ConcurrentHashMap(可以看我之前的对于ConcurrentHashMap原理分析文章)
2、线程安全的List
Vector和Collections.synchronizedList()方法,此时是使用synchronized关键字来保证线程安全。
使用更加高效的CopyOnWriteArrayList、ConcurrentLinkedQueue来保证线程安全
下面则对JUC提供的几个并发容器进行分析(ConcurrentHashMap可以看我之前写的文章)
二、高效读写大的并发队列ConcurrentLinkedQueue
ConcurrentLinkedQueue是由链表实现的复杂的高并发下的高效并发队列。在ConcurrentLinkedQueue类中,定义的节点Node核心如下:
其中item是用来存放目标元素数据的,如果存放的是String类型,则item就是String类型。
next表示当前node节点的下一个元素。此时ConcurrentLinkedQueue类的基本结果:
对Node进行操作时,使用的是CAS操作。
可以从这里看出,tail并不是一直再更新。这里我们来看看ConcurrentLinkedQueue提供的offeer方法(往队列添加元素)和poll方法(弹出队列的元素)核心代码是怎么实现的??
1、offer方法(这里用的是jdk 7u40)上面就表明了tail的更新是滞后的。下面则通过看poll的源码看看哨兵节点(next指向自己)是如何产生的?
2、poll方法上述的poll方法如下:
三、CopyOnWriteArrayList
读不加锁,写不会阻塞读取操作。只有写入和写入之间互斥。
写操作时,复制一份数据副本,然后在副本上进行更新数据。修改完后就用副本替换原来的数据。这样就不会影响读操作去读取原数据。
读操作:
四、数据共享通道:BlockingQueue
如何进行多个线程间的数据共享?例如线程A希望给线程B发送一条信息,此时用什么方式告知线程B比较合适?
此时我们可以设计一个程序,此时我们希望线程A能够通知线程B,又希望线程A不知道线程B的存在。这样我们如果之后要进行重构或者升级,我们完全可以不修改线程A,此时直接把线程B升级为线程C,这样能够保证系统的平滑过渡。那么这个时候就需要BlockingQueue来充当中间人的角色。BlockingQueue和之前介绍的CopyOnWriteArrayList等是不同的,BlockingQueue是一个接口,其主要实现有下面几种:
之前线程池的任务队列有些就是用到这里的。
这里主要介绍ArrayBlockingQueue和LinkedBlockingQueue类。从名字可以得知分别是基于数组和链表实现的。
因此ArrayBlockingQueue更适合做有界队列,因为队列可容纳的最大元素需要在队列创建时指定(毕竟数组的动态扩展不太方便)。而LinkedBlockingQueue更适合做无界队列,或者边界值非常大的队列,因为其内部元素可以动态扩容。
而BlockingQueue之所以适合作为数据共享的通道,关键还在Blocking上。Blocking即阻塞的意思,当线程处理完队列中的所有任务后,那它怎么知道下一个任务来了?
这个时候BlockingQueue很好的解决了这个方法,他会让服务线程在队列为空时进行阻塞等待,当有任务进入队列后自动将线程唤醒。此时我们以ArrayBlockingQueue为例:
ArrayBlockingQueue类的内部元素都放置在一个对象数组中:即利用了重入锁的机制的Condition的await和signal机制进行线程的阻塞和唤醒。
五、随机数据结构:跳表(SkipList)
跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。但不同于平衡树的是,对于平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整,而对于跳表的插入和删除只需要对整个数据结构的局部进行操作即可。
这样在高并发下你需要用一个全局的锁来保证平衡树的线程安全,而对于跳表你只需要进行部分锁即可,相比之下,跳表则拥有更好的性能,就查询性能跳表的时间复杂度是O(log n)
跳表的另一个特点是随机算法。跳表的本质是同时维护了多个链表,并且链表是分层的。具体的跳表结构示意图
最底层的链表维护着跳表的所有元素,每上一层链表就是下面一层链表的子集,一个元素插入到哪个层是完全随机的,因此如果运气不好,你得到的那一层链表会是个性能比较低的链表层。
跳表内的元素都是排序的。查找时先从顶级链表开始找,一旦发现节点的值大于要查找的值(或者查完没用对应的值),此时就跳到下面一层进行查找,所以这种查找是跳跃性的。
例如我现在要查询7这个值,此时跳表结构如下:
此时找第一层,没有比7大的,所以快速进入第二层的6的节点,然后走到8发现8比7大,所以此时跳入第三层的值为6的节点,然后在第三层就能快速的找到7了。
很显然,跳表是一种空间换时间的算法。使用跳表实现的map和使用哈希实现的map的另外一个不同是:哈希不会保存元素的顺序,而跳表的所有元素都是有序的。因此对跳表进行遍历的时候你会得到一个有序的结果。
下面演示简单的使用ConcurrentSkipMap
跳表的内部实现由几个关键的数据结构组成。Node节点,里面包含key和value(就是Map的key和value),每个Node还会指向下一个Node(next元素)。
其中对于Node的操作都是使用CAS方法:
二、使用JMH进行性能测试
java微基准测试框架JMH,其精度可达毫秒级,OpenJDK项目中发布的,通过JMH可以对多个方法的性能进行定量分析。
比如知道执行一个函数需要多少时间,或者对于多个算法有多种实现的时候需要选择性能最好的那个。
1、简单的使用JMH
基本使用可以看:https://www.zhihu.com/question/276455629/answer/1259967560
2、JMH的基本概念和配置
1)、模式(Mode)
表示JMH的测量方式和角度,共有4种:
2)、迭代(Iteration)
3)、预热(Warmup)
4)、状态(State)
5)、配置类(Options/OptionsBuilder)
3、理解JMH的Mode
在JMH中吞吐量和平均时间是最常用的。
吞吐量的测试方法:
平均时间的测试方式设置:
4、理解JMH中的state
自己可以对CopyOnWriteArrayList和ConcurrentLinkedQueue进行JMH的性能测试看看各自的性能在不同的场景下性能如何。