ConcurrentHashMap的施用技巧

ConcurrentHashMap的使用技巧
在日常开发中,资源池是经常遇到的场景,一种简单的实现是按需创建一个资源,然后放入map中缓存起来,后续使用这个资源时直接从map中获取.

最简单可靠的实现是利用HashedMap+synchronized(或者Lock)
这种方式无疑是正确的,但锁的粒度较大,高并发时性能不佳

改进的一种典型思路是利用JUC里的并发工具ConcurrentHashMap,降低锁粒度,提高并发性
http://dmy999.com/article/34/correct-use-of-concurrenthashmap里提到了一种实践,主要代码如下
private ConcurrentMap records = new ConcurrentHashMap();

private Record getOrCreate(String id) {
    Record rec = records.get(id);
    if (rec == null) {
        // record does not yet exist
        Record newRec = new Record(id);
        rec = records.putIfAbsent(id, newRec);
        if (rec == null) {
            // put succeeded, use new value
            rec = newRec;
        }
    }
    return rec;
}

这种方法直接使用ConcurrentMap,且没有外层的锁,
putIfAbsent方法不会重复放入相同key的对象,
ConcurrentMap内部实现也保证了内存可见性.
看起来是基本正确的.而且大部分情况也是可以正常工作的.

但是这种写法有个致命的问题,那就是
Record newRec = new Record(id);
这个地方在并发情况下可能被多次执行.意味着资源被多次创建.
如果这个资源没有什么外部依赖,多次创建倒是没有问题,不过当遇到
依赖外部资源(文件,socket)时,多次创建就会有报错.

要避免这种资源的多次创建,可以使用double checked的方式实现.
虽然double checked由来已久,且有著名的double checked locking问题
关于此问题(请参见http://en.wikipedia.org/wiki/Double-checked_locking
)的产生主要是java内存模型导致的,但是使用volatile可以避免lock的问题
可能早些时候很多著作里都会提到volatile代价太高,但随着jvm发展(1.5以后),volatile的实现
也变得轻量高效,具体的论述可以参见http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#dcl
注意页面里划掉的一大段论述

Double checked最早是为了解决单例模式产生的,目前java的单例模式已经有更好的
实现方式,比如static变量(dcl的wiki描述),比如enum单例(参见effective java 2nd)

但是在资源池场景仍然可以使用Double checked保证我们资源仅被创建一次
同时尽量降低同步的代价
代码如下
private ConcurrentMap records = new ConcurrentHashMap();

private Record getOrCreate(String id) {
    Record rec = records.get(id);
    if (rec == null) {
        synchronized (this) {
            rec = records.get(id);
            if (rec == null) {
                // record does not yet exist
                Record newRec = new Record(id);
                rec = records.putIfAbsent(id, newRec);
                if (rec == null) {
                    // put succeeded, use new value
                   rec = newRec;
                }
            }
        }
    }
    return rec;
}


注意此处使用了sychronized,仍然不能使用HashedMap代替ConcurrentHashMap
因为这种写法有多线程同时访问Map的get和put方法的可能,
而HashedMap的get和put方法并发环境下也是会出错的.