前面说了同步容器类(如Vector,synchronizedList等),所有的操作都会加一个锁,同步容器将所有的操作都串行化,虽然安全了,但是严重降低了并发性,多线程竞争时,吞吐量也严重降低。
JDK1.5之后新增了一些并发容器,可以在多线程并发访问情况下极大的提高伸缩性,并且降低风险。如BlockingQueue、ConcurrentMap、CopyOnWriteArrayList等等,这里我们说说ConcurrentHashMap和CopyOnWriteArrayList。
ConcurrentHashMap和HashMap一样,都是基于散列的Map,但是它支持并发,实现原理和同步容器有所不同,同步容器是使用一个锁,当并发访问时,尤其在散列表分布不均匀时,遍历会花费很长时间,要是在某些甚至全部元素上调用equal方法,就会花费更长的时间,而其他线程在这段时间内只能等待。
而ConcurrentHashMap使用多个锁的机制,这种机制称为“分段锁”,其实可以理解为将原有的一个Hash表分成了多段Hash表(segment),针对每个segment有一个锁。这种机制下,允许一定数量线程并发地修改Map(因为修改不同的segment嘛,如果修改相同的segment,就要等待了),这样更细粒度的加锁机制就能实现更大程度的共享。
![](https://img-blog.csdn.net/20140408125204046?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveHVlcGlhb2hhbjIwMDY=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
这样就能实现更高的吞吐量了,而且在单线程环境中,损失的性能也比较小。ConcurrentHashMap在迭代时不会抛出ConcurrentModificationException,因为不需要在迭代的过程中对整个容器加锁。
当然,有些涉及跨段(操作全部segment)的操作怎么办呢,比如size(),isEmpty()等方法。拿size()方法举例,size()方法内部是获取全部的segment的元素的和,如果失败(可能有其他线程正在修改容器结构),会重试RETRIES_BEFORE_LOCK次,如果还失败,则持有全部segment的锁,然后再求和。
可以想想出,不对整个容器加锁的情况下,size()方法可能得到一个已经过期的值,有时候可能不精确。其实这正是ConcurrentHashMap的不足之处。其实并发情况下,这些操作的用处都很小,因为他们返回值总在变化,因此这些操作的需求被弱化了,牺牲了这些操作的精确性换取了其他更重要操作性能的提高,如果你需要的场景必须依赖这些操作的精确性,那只有使用Hashtable,synchronizedMap这类的同步容器了,这些操作还包括get,put,containsKey,remove等。
引用别人的评价就是,ConcurrentHashMap是一个支持高并发的高性能的HashMap实现,它支持完全并发的读以及一定程度并发的写。ConcurrentHashMap的实现也是很精巧,充分利用了最新的JVM规范,值得学习,却不值得模仿。
接下来说说CopyOnWriteArrayList,它是用来代替同步List(如Vector,synchronizedList等)的。某些情况下(后面会说什么情况),它能提供非常好的并发性能,更主要的是,这个容器在迭代时不需要加锁。
看这个类的名字就能猜出其原理,Copy-On-Write,写入时复制。我们都知道并发情况下,List迭代时是要加锁的,目的是防止别的线程在此期间修改容器,而CopyOnWriteArrayList容器类在修改容器时是Copy了一份原始容器的副本,针对副本进行修改,完毕后将原始容器指针指向副本,从而实现了修改容器,但是不影响原始容器,而正在迭代原始容器的线程也不会受到影响。这样在读取时是不用加锁的,从而实现了读取操作的性能提升,但是修改(增删等)操作还是要加锁的,防止并发修改问题。
了解了原理,就能发现一个问题,每次修改容器时,都会Copy一个副本,这个性能开销好大呀,尤其容器规模较大时。这也正是这个容器的不足之处,不能支持频繁,大量的修改操作。也是前面说的这个容器使用的某些情况,仅当迭代操作远远多于修改操作时,才能使用这个容器。在这个准则下,举几个简单的场景,如观察者模式中,将观察者放到CopyOnWriteArrayList中,并发注册和删除观察者(修改容器)的频率很少,但是并发通知观察者(迭代容器)的情况非常多;还有事件监听器,注册和注销事件的操作远远少于接收事件通知的操作;还有一些缓存信息,读取非常频繁,更新修改比较少等。
CopyOnWriteArrayList还有个地方要说明,就是在一个线程正在修改,还没修改完时,另一个线程开始迭代,此时迭代的是没修改之前的容器,如果这个不适合你的使用场景(比如你必须要得到修改完之后的容器),也可以使用读写锁来进行控制。
JDK1.5之后新增了一些并发容器,可以在多线程并发访问情况下极大的提高伸缩性,并且降低风险。如BlockingQueue、ConcurrentMap、CopyOnWriteArrayList等等,这里我们说说ConcurrentHashMap和CopyOnWriteArrayList。
ConcurrentHashMap和HashMap一样,都是基于散列的Map,但是它支持并发,实现原理和同步容器有所不同,同步容器是使用一个锁,当并发访问时,尤其在散列表分布不均匀时,遍历会花费很长时间,要是在某些甚至全部元素上调用equal方法,就会花费更长的时间,而其他线程在这段时间内只能等待。
而ConcurrentHashMap使用多个锁的机制,这种机制称为“分段锁”,其实可以理解为将原有的一个Hash表分成了多段Hash表(segment),针对每个segment有一个锁。这种机制下,允许一定数量线程并发地修改Map(因为修改不同的segment嘛,如果修改相同的segment,就要等待了),这样更细粒度的加锁机制就能实现更大程度的共享。
这样就能实现更高的吞吐量了,而且在单线程环境中,损失的性能也比较小。ConcurrentHashMap在迭代时不会抛出ConcurrentModificationException,因为不需要在迭代的过程中对整个容器加锁。
当然,有些涉及跨段(操作全部segment)的操作怎么办呢,比如size(),isEmpty()等方法。拿size()方法举例,size()方法内部是获取全部的segment的元素的和,如果失败(可能有其他线程正在修改容器结构),会重试RETRIES_BEFORE_LOCK次,如果还失败,则持有全部segment的锁,然后再求和。
可以想想出,不对整个容器加锁的情况下,size()方法可能得到一个已经过期的值,有时候可能不精确。其实这正是ConcurrentHashMap的不足之处。其实并发情况下,这些操作的用处都很小,因为他们返回值总在变化,因此这些操作的需求被弱化了,牺牲了这些操作的精确性换取了其他更重要操作性能的提高,如果你需要的场景必须依赖这些操作的精确性,那只有使用Hashtable,synchronizedMap这类的同步容器了,这些操作还包括get,put,containsKey,remove等。
引用别人的评价就是,ConcurrentHashMap是一个支持高并发的高性能的HashMap实现,它支持完全并发的读以及一定程度并发的写。ConcurrentHashMap的实现也是很精巧,充分利用了最新的JVM规范,值得学习,却不值得模仿。
接下来说说CopyOnWriteArrayList,它是用来代替同步List(如Vector,synchronizedList等)的。某些情况下(后面会说什么情况),它能提供非常好的并发性能,更主要的是,这个容器在迭代时不需要加锁。
看这个类的名字就能猜出其原理,Copy-On-Write,写入时复制。我们都知道并发情况下,List迭代时是要加锁的,目的是防止别的线程在此期间修改容器,而CopyOnWriteArrayList容器类在修改容器时是Copy了一份原始容器的副本,针对副本进行修改,完毕后将原始容器指针指向副本,从而实现了修改容器,但是不影响原始容器,而正在迭代原始容器的线程也不会受到影响。这样在读取时是不用加锁的,从而实现了读取操作的性能提升,但是修改(增删等)操作还是要加锁的,防止并发修改问题。
了解了原理,就能发现一个问题,每次修改容器时,都会Copy一个副本,这个性能开销好大呀,尤其容器规模较大时。这也正是这个容器的不足之处,不能支持频繁,大量的修改操作。也是前面说的这个容器使用的某些情况,仅当迭代操作远远多于修改操作时,才能使用这个容器。在这个准则下,举几个简单的场景,如观察者模式中,将观察者放到CopyOnWriteArrayList中,并发注册和删除观察者(修改容器)的频率很少,但是并发通知观察者(迭代容器)的情况非常多;还有事件监听器,注册和注销事件的操作远远少于接收事件通知的操作;还有一些缓存信息,读取非常频繁,更新修改比较少等。
CopyOnWriteArrayList还有个地方要说明,就是在一个线程正在修改,还没修改完时,另一个线程开始迭代,此时迭代的是没修改之前的容器,如果这个不适合你的使用场景(比如你必须要得到修改完之后的容器),也可以使用读写锁来进行控制。