Java并发包中常用类小结(一)
从JDK1.5以后,Java为我们引入了一个并发包,用于解决实际开发中经常用到的并发问题,那我们今天就来简单看一下相关的一些常见类的使用情况。
1、ConcurrentHashMap
ConcurrentHashMap其实就是线程安全版本的hashMap。前面我们知道HashMap是以链表的形式存放hash冲突的数据,以数组形式存放HashEntry等hash出来不一致的数据。为了保证容器的数据一致性,需要加锁。HashMap的实现方式是,只有put和remove的时候会引发数据的不一致,那为了保证数据的一致性,我在put和remove的时候进行加锁操作。但是随之而来的是性能问题,因为key-value形式的数据,读写频繁是很正常的,也就意味着我有大量数据做读写操作时会引发长时间的等待。为了解决这个问题,Java并发包问我们提供了新的思路。在每一个HashEntry上加一把锁,对于hash冲突的数据,因为采用链表存储,公用一把锁。这样我才在做不同hash数值的数据时,则是在不同的锁环境下执行,基本上是互不干扰的。在最好情况下,可以保证16个线程同时进行无阻塞的操作(HashMap的默认HashEntry是16,亦即默认的数组大小是16)。
那ConcurrentHashMap是如何保证数据操作的一致性呢?对于数据元素的大小,ConcurrentHashMap将对应数组(HashEntry的长度)的变量为voliate类型的,也就是任何HashEntry发生变更,所有的地方都会知道数据的大小。对于元素,如何保证我取出的元素的next不发生变更呢?(HashEntry中的数据采用链表存储,当读取数据的时候可能又发生了变更),这一点,ConcurrentHashMap采取了最简单的做法,hash值、key和next取出后都为final类型的,其next等数据永远不会发生变更。
另外ConcurrentHashMap采用的锁结构是将读和写分开的,大大的提升了性能,下面我们来看一下两者之间的性能差。
由于数据比较密集,我们分开来看一下
相关数据如下:
分析表 |
元素个数 |
10 |
100 |
1000 |
10000 |
||||||||
线程数 |
容器类别 |
增加 |
删除 |
查找 |
增加 |
删除 |
查找 |
增加 |
删除 |
查找 |
增加 |
删除 |
查找 |
1 |
HashMap |
2805 |
1743 |
1520 |
3004 |
1726 |
1579 |
1995 |
1846 |
1528 |
2032 |
1787 |
1501 |
ConcurrentHashMap |
4947 |
2010 |
1699 |
5292 |
2005 |
1661 |
2322 |
1842 |
1243 |
2351 |
2113 |
1541 |
|
10 |
HashMap |
29814 |
36539 |
28076 |
31180 |
55178 |
38156 |
31217 |
36756 |
31785 |
33314 |
30497 |
26488 |
ConcurrentHashMap |
18364 |
22086 |
8064 |
21420 |
22805 |
9932 |
20164 |
20875 |
7800 |
19383 |
19483 |
10254 |
|
50 |
HashMap |
233674 |
193918 |
230404 |
205577 |
221995 |
213651 |
343005 |
318603 |
343153 |
249921 |
229954 |
234555 |
ConcurrentHashMap |
131573 |
98534 |
16778 |
152609 |
96412 |
24233 |
123199 |
108388 |
20156 |
134971 |
122927 |
18799 |
|
100 |
HashMap |
313442 |
309336 |
302591 |
332389 |
314167 |
296360 |
343005 |
318603 |
343153 |
329171 |
352704 |
354593 |
ConcurrentHashMap |
161866 |
122582 |
21369 |
141274 |
114333 |
21875 |
116758 |
97985 |
24098 |
140902 |
120459 |
18766 |
(神马情况 数据看不到了 弄一个图吧)
我们可以看到,在单线程下,ConcurrentHashMap的综合性能略低于HashMap,但是随着线程的增长,ConcurrentHashMap的优势就明显提现出来了,尤其是查找元素的性能。因此并发情况下,ConcurrentHashMap是代替HashMap的一个不错的选择。
2、CopyOnWriteArrayList
同样的,CopyOnWriteArrayList是线程安全版本的ArrayList。和ArrayList不同的是,CopyOnWriteArrayList默认是创建了一个大小为0的容器。通过ReentrantLock来保证线程安全。CopyOnWriteArrayList其实每次增加的时候,需要新创建一个比原来容量+1大小的数组,然后拷贝原来的元素到新的数组中,同时将新插入的元素放在最末端。然后切换引用。
针对CopyOnWriteArrayList,因为每次做插入和删除操作,都需要重新开辟空间和复制数组元素,因此对于插入和删除元素,CopyOnWriteArrayList的性能远远不如ArrayList,但是每次读取的时候,CopyOnWriteArrayList在不加锁的情况下直接锁定数据,会快很多(但是可能会引发脏读),对于迭代,CopyOnWriteArrayList会生成一个快照数组,因此当迭代过程中出现变化,快照数据没有变更,因此读到的数据也是不会变化的。在读多写少的环境下,CopyOnWriteArrayList的性能还是不错的。
3、CopyOnWriteArraySet
CopyOnWriteArraySet是基于CopyOnWriteArrayList实现的。但是CopyOnWriteArraySet鉴于不能插入重复数据,因此每次add的时候都要遍历数据,性能略低于CopyOnWriteArrayList。
4、ArrayBlockingQueue
ArrayBlockingQueue是基于数组实现的一个线程安全的队列服务,其相关的功能前面我们已经用到过了,这里就不多提了。
5、Atomic类,如AtomicInteger、AtomicBoolean
我们来看以下对应的API文档
这种原子类是基于JDK的CAS的无阻塞操作,比我们写同步的效率要高多了哦。
对于Atomic类我们在后面的示例中也会很频繁的使用,这里也就不多介绍了。