深入浅出 Java Concurrency (16): 并发容器 part 1 ConcurrentMap (1)[转]

从这一节开始正式进入并发容器的部分,来看看JDK 6带来了哪些并发容器。

在JDK 1.4以下只有Vector和Hashtable是线程安全的集合(也称并发容器,Collections.synchronized*系列也可以看作是线程安全的实现)。从JDK 5开始增加了线程安全的Map接口ConcurrentMap和线程安全的队列BlockingQueue(尽管Queue也是同时期引入的新的集合,但是规范并没有规定一定是线程安全的,事实上一些实现也不是线程安全的,比如PriorityQueue、ArrayDeque、LinkedList等,在Queue章节中会具体讨论这些队列的结构图和实现)。

在介绍ConcurrencyMap之前先来回顾下Map的体系结构。下图描述了Map的体系结构,其中蓝色字体的是JDK 5以后新增的并发容器。

针对上图有以下几点说明:

  1. Hashtable是JDK 5之前Map唯一线程安全的内置实现(Collections.synchronizedMap不算)。特别说明的是Hashtable的t是小写的(不知道为啥),Hashtable继承的是Dictionary(Hashtable是其唯一公开的子类),并不继承AbstractMap或者HashMap。尽管Hashtable和HashMap的结构非常类似,但是他们之间并没有多大联系。
  2. ConcurrentHashMap是HashMap的线程安全版本,ConcurrentSkipListMap是TreeMap的线程安全版本。
  3. 最终可用的线程安全版本Map实现是ConcurrentHashMap/ConcurrentSkipListMap/Hashtable/Properties四个,但是Hashtable是过时的类库,因此如果可以的应该尽可能的使用ConcurrentHashMap和ConcurrentSkipListMap。

回到正题来,这个小节主要介绍ConcurrentHashMap的API以及应用,下一节才开始将原理和分析。

除了实现Map接口里面对象的方法外,ConcurrentHashMap还实现了ConcurrentMap里面的四个方法。

V putIfAbsent(K key,V value)

如果不存在key对应的值,则将value以key加入Map,否则返回key对应的旧值。这个等价于清单1 的操作:

清单1 putIfAbsent的等价操作

if (!map.containsKey(key)) 
   return map.put(key, value);
else
   return map.get(key);

在前面的章节中提到过,连续两个或多个原子操作的序列并不一定是原子操作。比如上面的操作即使在Hashtable中也不是原子操作。而putIfAbsent就是一个线程安全版本的操作的。

有些人喜欢用这种功能来实现单例模式,例如清单2。

清单2 一种单例模式的实现

package xylz.study.concurrency;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class ConcurrentDemo1 {

private static final ConcurrentMap<String, ConcurrentDemo1> map = new ConcurrentHashMap<String, ConcurrentDemo1>();
    private static ConcurrentDemo1 instance;
    public static ConcurrentDemo1 getInstance() {
        if (instance == null) {

map.putIfAbsent("INSTANCE", new ConcurrentDemo1());

instance = map.get("INSTANCE");
        }
        return instance;
    }

private ConcurrentDemo1() {
    }

}

当然这里只是一个操作的例子,实际上在单例模式文章中有很多的实现和比较。清单2 在存在大量单例的情况下可能有用,实际情况下很少用于单例模式。但是这个方法避免了向Map中的同一个Key提交多个结果的可能,有时候在去掉重复记录上很有用(如果记录的格式比较固定的话)。

boolean remove(Object key,Object value)

只有目前将键的条目映射到给定值时,才移除该键的条目。这等价于清单3 的操作。

清单3 remove(Object,Object)的等价操作

if (map.containsKey(key) && map.get(key).equals(value)) {
   map.remove(key);
   return true;
}
return false;

由于集合类通常比较的hashCode和equals方法,而这两个方法是在Object对象里面,因此两个对象如果hashCode一致,并且覆盖了equals方法后也一致,那么这两个对象在集合类里面就是“相同”的,不管是否是同一个对象或者同一类型的对象。也就是说只要key1.hashCode()==key2.hashCode() && key1.equals(key2),那么key1和key2在集合类里面就认为是一致,哪怕他们的Class类型不一致也没关系,所以在很多集合类里面允许通过Object来类型来比较(或者定位)。比如说Map尽管添加的时候只能通过制定的类型<K,V>,但是删除的时候却允许通过一个Object来操作,而不必是K类型。

既然Map里面有一个remove(Object)方法,为什么ConcurrentMap还需要remove(Object,Object)方法呢?这是因为尽管Map里面的key没有变化,但是value可能已经被其他线程修改了,如果修改后的值是我们期望的,那么我们就不能拿一个key来删除此值,尽管我们的期望值是删除此key对于的旧值。

这种特性在原子操作章节的AtomicMarkableReferenceAtomicStampedReference里面介绍过。

boolean replace(K key,V oldValue,V newValue)

只有目前将键的条目映射到给定值时,才替换该键的条目。这等价于清单4 的操作。

清单4 replace(K,V,V)的等价操作

if (map.containsKey(key) && map.get(key).equals(oldValue)) {
   map.put(key, newValue);
   return true;
}
return false;

V replace(K key,V value)

只有当前键存在的时候更新此键对于的值。这等价于清单5 的操作。

清单5 replace(K,V)的等价操作

if (map.containsKey(key)) {
   return map.put(key, value);
}
return null;

replace(K,V,V)相比replace(K,V)而言,就是增加了匹配oldValue的操作。

其实这4个扩展方法,是ConcurrentMap附送的四个操作,其实我们更关心的是Map本身的操作。当然如果没有这4个方法,要完成类似的功能我们可能需要额外的锁,所以有总比没有要好。比如清单6,如果没有putIfAbsent内置的方法,我们如果要完成此操作就需要完全锁住整个Map,这样就大大降低了ConcurrentMap的并发性。这在下一节中有详细的分析和讨论。

清单6 putIfAbsent的外部实现

public V putIfAbsent(K key, V value) {
    synchronized (map) {
        if (!map.containsKey(key)) return map.put(key, value);
        return map.get(key);
    }
}

参考资料:

时间: 2024-10-06 23:53:03

深入浅出 Java Concurrency (16): 并发容器 part 1 ConcurrentMap (1)[转]的相关文章

深入浅出 Java Concurrency (17): 并发容器 part 2 ConcurrentMap (2)

本来想比较全面和深入的谈谈ConcurrentHashMap的,发现网上有很多对HashMap和ConcurrentHashMap分析的文章,因此本小节尽可能的分析其中的细节,少一点理论的东西,多谈谈内部设计的原理和思想. 要谈ConcurrentHashMap的构造,就不得不谈HashMap的构造,因此先从HashMap开始简单介绍. HashMap原理 我们从头开始设想.要将对象存放在一起,如何设计这个容器.目前只有两条路可以走,一种是采用分格技术,每一个对象存放于一个格子中,这样通过对格子

深入浅出 Java Concurrency (17): 并发容器 part 2 ConcurrentMap (2)[转]

本来想比较全面和深入的谈谈ConcurrentHashMap的,发现网上有很多对HashMap和ConcurrentHashMap分析的文章,因此本小节尽可能的分析其中的细节,少一点理论的东西,多谈谈内部设计的原理和思想. 要谈ConcurrentHashMap的构造,就不得不谈HashMap的构造,因此先从HashMap开始简单介绍. HashMap原理 我们从头开始设想.要将对象存放在一起,如何设计这个容器.目前只有两条路可以走,一种是采用分格技术,每一个对象存放于一个格子中,这样通过对格子

深入浅出 Java Concurrency (18): 并发容器 part 3 ConcurrentMap (3)[转]

在上一篇中介绍了HashMap的原理,这一节是ConcurrentMap的最后一节,所以会完整的介绍ConcurrentHashMap的实现. ConcurrentHashMap原理 在读写锁章节部分介绍过一种是用读写锁实现Map的方法.此种方法看起来可以实现Map响应的功能,而且吞吐量也应该不错.但是通过前面对读写锁原理的分析后知道,读写锁的适合场景是读操作>>写操作,也就是读操作应该占据大部分操作,另外读写锁存在一个很严重的问题是读写操作不能同时发生.要想解决读写同时进行问题(至少不同元素

深入浅出 Java Concurrency (21): 并发容器 part 6 可阻塞的BlockingQueue (1)[转]

在<并发容器 part 4 并发队列与Queue简介>节中的类图中可以看到,对于Queue来说,BlockingQueue是主要的线程安全版本.这是一个可阻塞的版本,也就是允许添加/删除元素被阻塞,直到成功为止. BlockingQueue相对于Queue而言增加了两个操作:put/take.下面是一张整理的表格. 看似简单的API,非常有用.这在控制队列的并发上非常有好处.既然加入队列和移除队列能够被阻塞,这在实现生产者-消费者模型上就简单多了. 清单1 是生产者-消费者模型的一个例子.这个

深入浅出 Java Concurrency (27): 并发容器 part 12 线程安全的List/Set[转]

本小节是<并发容器>的最后一部分,这一个小节描述的是针对List/Set接口的一个线程版本. 在<并发队列与Queue简介>中介绍了并发容器的一个概括,主要描述的是Queue的实现.其中特别提到一点LinkedList是List/Queue的实现,但是LinkedList确实非线程安全的.不管BlockingQueue还是ConcurrentMap的实现,我们发现都是针对链表的实现,当然尽可能的使用CAS或者Lock的特性,同时都有通过锁部分容器来提供并发的特性.而对于List或者

深入浅出 Java Concurrency (25): 并发容器 part 10 双向并发阻塞队列 BlockingDeque[转]

这个小节介绍Queue的最后一个工具,也是最强大的一个工具.从名称上就可以看到此工具的特点:双向并发阻塞队列.所谓双向是指可以从队列的头和尾同时操作,并发只是线程安全的实现,阻塞允许在入队出队不满足条件时挂起线程,这里说的队列是指支持FIFO/FILO实现的链表. 首先看下LinkedBlockingDeque的数据结构.通常情况下从数据结构上就能看出这种实现的优缺点,这样就知道如何更好的使用工具了. 从数据结构和功能需求上可以得到以下结论: 要想支持阻塞功能,队列的容量一定是固定的,否则无法在

深入浅出 Java Concurrency (23): 并发容器 part 8 可阻塞的BlockingQueue (3)[转]

在Set中有一个排序的集合SortedSet,用来保存按照自然顺序排列的对象.Queue中同样引入了一个支持排序的FIFO模型. 并发队列与Queue简介 中介绍了,PriorityQueue和PriorityBlockingQueue就是支持排序的Queue.显然一个支持阻塞的排序Queue要比一个非线程安全的Queue实现起来要复杂的多,因此下面只介绍PriorityBlockingQueue,至于PriorityQueue只需要去掉Blocking功能就基本相同了. 排序的Blocking

深入浅出 Java Concurrency (26): 并发容器 part 11 Exchanger[转]

可以在对中对元素进行配对和交换的线程的同步点.每个线程将条目上的某个方法呈现给 exchange 方法,与伙伴线程进行匹配,并且在返回时接收其伙伴的对象.Exchanger 可能被视为 SynchronousQueue 的双向形式. 换句话说Exchanger提供的是一个交换服务,允许原子性的交换两个(多个)对象,但同时只有一对才会成功.先看一个简单的实例模型. 在上面的模型中,我们假定一个空的栈(Stack),栈顶(Top)当然是没有元素的.同时我们假定一个数据结构Node,包含一个要交换的元

深入浅出 Java Concurrency (20): 并发容器 part 5 ConcurrentLinkedQueue[转]

ConcurrentLinkedQueue是Queue的一个线程安全实现.先来看一段文档说明. 一个基于链接节点的无界线程安全队列.此队列按照 FIFO(先进先出)原则对元素进行排序.队列的头部 是队列中时间最长的元素.队列的尾部 是队列中时间最短的元素.新的元素插入到队列的尾部,队列获取操作从队列头部获得元素.当多个线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择.此队列不允许使用 null 元素. 由于ConcurrentLinke