免锁容器之CopyOnWriteArrayList、ConcurrentHashMap

    像Vector 和 Hashtable 这类早期容器具有许多synchronized方法,当他们用于非多线程的应用程序时,便会导致不可接受的开销。在Java1.2中,新的容器类库是不同步的,并且Collections类提供了各种static同步的装饰方法,从而来同步不同类型的容器。尽管这是一种改进,因为它使你可以选择在你的容器中是否要使用同步,但是这种开销依旧是基于synchronized加锁机制的。Java SE5特别添加了新的容器,通过使用更灵巧的技术来消除加锁,从而提高线程安全的性能。

 

    这些免锁容器背后的通用策略是:对容器的修改可以与读取操作同时发生,只要读取者只能看到完成修改的结果即可。修改是在容器数据结构的某个部分的一个单独的副本(有时是整个数据结构的副本)上执行的,并且这个副本在修改过程中是不可视的。只有当修改完成时,被修改的结构才会主动地与主数据结构进行交换,之后读取者就可以看到这个修改了。(Copy on Write

 

 

    CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而原数组将保留在原地,使得复制的数据在被修改时,读取操作可以安全地执行。当修改完成时,一个原子性的操作将把新的数组换入,使得新的出去操作可以看到这个新的修改

 

    CopyOnWriteArrayList的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出ConcurrentModificationException(在 for each 中删除元素会抛出 ConcurrentModificationException)。

 

    CopyOnWriteArraySet 使用 CopyOnWriteArrayList来实现其免锁行为。

 

    ConcurrentHashMap 和 ConcurrentLinkedQueue使用了类似的技术,允许并发的读取和写入,但是容器中只有部分内容而不是整个容器可以被复制和修改。然而,任何修改在完成之前,读取者仍旧不能看到它们。ConcurrentHashMap不会抛出ConcurrentModificationException异常。

注:https://stackoverflow.com/a/15943555/6214310 这里提到ConcurrentHashMap 并不会创建底层数据结构的副本,这可能也是一个有CopyOnWrite前缀而一个则直接是Concurrent前缀的原因。

ConcurrentHashMap 、 Hashtable 、HashMap的异同

ConcurrentHashMap 在bucket扩大时(put()时元素数量过多时,需要扩大槽位,这时hash算法也会随之变)会把 oldTable 中的Entry按照新的hash算法重新放入newTable中。完成后 把newTable对象。赋值给ConcurrentHashMap 的 table引用。

 

ConcurrentHashMap put remove replace 这些修改操作都是 用Segment 加锁的,Segment 继承了ReentrantLock,Segment就相当于table的一个槽位,所以put时只锁所在槽位,remove replace同理,get 等查询操作没有加锁的操作。与Hashtable比起来 Hashtable 几乎所有的方法都是在方法上用了 synchronized关键字,不仅查询 修改等方法都要等待,一旦等待便是整个表的数据都要等待,而ConcurrentHashMap 查询不锁修改只锁槽位效率更好。

 

HashMap Hashtable ConcurrentHashMap 添加一个新的键值对都是把它放入到所在槽位链表的头上,所以槽位上的Entry也就是就是此Entry的引用。当get()时根据hash找到该槽位,从链表的头结点开始遍历到尾结点,发现hash值且equals匹配的便返回此Entry的value。

 

乐观锁

    只要你主要是从免锁容器中读取,那么它就会比其synchronized对应物快许多,因为获取和释放锁的开销被省掉了。如果需要向免锁容器中执行少量写入,那么情况依旧如此。

 

 

从测试结果来看,synchronized ArrayList无论读取者和写入者的数量是多少,都具有大致相同的性能——读取者与其他读取者竞争锁的方式与写入者相同。但是CopyOnWriteArrayList在没有写入者时,速度会快许多,并且在有五个写入者时,速度依旧明显的快。看起来你应该尽量使用CopyOnWriteArrayList,对列表写入的影响并没有超过短期同步整个列表的影响。当然,你必须在你的具体应用中尝试这两种不同的方式,以了解到底哪个更好一点。

 

    向ConcurrentHashMap添加写入者的影响甚至还不如CopyOnWriteArrayList明显,这是因为ConcurrentHashMap使用了一种不同的技术,它可以明显地最小化写入所造成的影响。

乐观加锁

Atomic对象可以执行像decrementAndGet()(i++)这样的原子操作,也允许“乐观加锁”。这就意味着执行某项计算时,实际上并没有使用互斥。但是在这项计算完成,并且你准备更新这个Atomic对象时,你需要使用一个称为compareAndSet()的方法。你将旧值和新值一起提交给这个方法,如果旧值和它在Atomic对象中发现的值不一致,那么这个操作就失败(返回false)——意味着某个其他任务已经于此操作执行期间修改了这个对象,但是这里我们是“乐观的”,因为我们保持数据为未锁定状态,并希望没有任何其他任务插入修改它。所以这些又都是以性能的名义执行的——通过Atomic来替代synchronized 或 Lock,可以获得性能上的好处。

如果compareAndSet()失败,那么就必须决定做点什么,如果不能执行某些恢复操作,那么你就不能使用这项技术,从而必须使用传统的互斥。你可能会重试这个操作,如果第二次成功,那么万事大吉:或者可能会忽略这次失败,直接结束。

 

非阻塞队列

非阻塞队列的特色就是队列里没有数据时,操作队列出现异常或返回null,不具有等待/阻塞的特色。

在JDK的并发包中,常见的非阻塞队列有:

  1. ConcurrentHashMap
  2. ConcurrentSkipListMap
  3. ConcurrentSkipListSet
  4. ConcurrentLinkedQueue
  5. ConcurrentLinkedDeque
  6. CopyOnWriteArrayList
  7. CopyOnWriteArraySet

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值