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类我们在后面的示例中也会很频繁的使用,这里也就不多介绍了。