ConcurrentHashMap与CAS

    |     2017年8月5日   |   多线程编程   |     0 条评论   |    2301

CAS(Compare and Swap)和 ConcurrentHashMap 经常绑在一起问:一个是硬件级的无锁更新,一个是并发 Map 怎么既线程安全又比整表锁更快。

JDK 5 之前同步几乎全靠 synchronized:竞争时上下文切换多、持锁会把别人挂起、还可能优先级倒置。volatile 能保证可见性,但不保证复合操作的原子性,所以还是要回到「锁」——只不过乐观锁用的是 CAS,而不是独占。

一、悲观锁、乐观锁与 CAS

类型 做法 典型
悲观 / 独占 先抢锁,别人等着;持锁线程不放,其余全挂起 synchronized
乐观 先假设没冲突做完,失败就重试直到成功 CAS / compareAndSet

CAS 三个操作数

内存位置 V、预期原值 A、新值 B。若 V 仍等于 A,处理器原子地把 V 改成 B;否则不动。无论成败都会带回该位置当前值(有的变体只返回成功与否)。口语就是:我认为 V 里是 A;若是,就写成 B;否则告诉我现在是多少。

常见用法:从 V 读出 A,算出 B,再用 CAS 把 V 从 A 改成 B。别人若已改过,CAS 失败,算法重算即可。Intel 上对应 cmpxchg;Java 早期用不上这些指令,有了 JNI 之后,Doug Lea 的 concurrent 包才把 CAS 当成整包基石。

AtomicInteger 怎么无锁 +1

没有锁时,值本身仍要 volatile,保证读到的是共享的最新值:

private volatile int value;
public final int get() {

        return value;

    }

incrementAndGet 是典型的自旋 CAS:

public final int incrementAndGet() {

    for (;;) {

        int current = get();

        int next = current + 1;

        if (compareAndSet(current, next))

            return next;

    }

}

compareAndSet 经 JNI 落到 CPU 指令:

public final boolean compareAndSet(int expect, int update) {

    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

    }

非阻塞算法的要求是:一个线程失败或挂起,不该拖死别的线程。compareAndSet 正是用硬件更新 + 冲突检测,代替整段加锁。

二、ConcurrentHashMap 的设计

并发里 ConcurrentHashMap 比 Hashtable 和 Collections.synchronizedMap() 写并发更好,代价是放宽了读的强一致性。

版本 锁策略 结构
JDK 7 分段锁 Segment:同段才竞争,不同段不抢 段数组 + 链表
JDK 8 去掉 Segment,节点上 CAS / synchronized,扩容也靠 CAS 数组 + 链表 + 红黑树,另有 TreeBin、Traverser 等辅助类

三、接口限流里的计数陷阱

统计 URL 调用次数,第一反应常是 ConcurrentHashMap<String, Long>。下面这段没显式加锁,输出却经常对不上:

public class CounterDemo1 {

    private final Map<String, Long> urlCounter = new ConcurrentHashMap<>();

    //接口调用次数+1

    public long increase(String url) {

        Long oldValue = urlCounter.get(url);

        Long newValue = (oldValue == null) ? 1L : oldValue + 1;

        urlCounter.put(url, newValue);

        return newValue;

    }

    //获取调用次数

    public Long getCount(String url){

        return urlCounter.get(url);

    }

    public static void main(String[] args) {

        ExecutorService executor = Executors.newFixedThreadPool(10);

        final CounterDemo1 counterDemo = new CounterDemo1();

        int callTime = 100000;

        final String url = "http://localhost:8080/hello";

        CountDownLatch countDownLatch = new CountDownLatch(callTime);

        //模拟并发情况下的接口调用统计

        for(int i=0;i<callTime;i++){

            executor.execute(new Runnable() {

                @Override

                public void run() {

                    counterDemo.increase(url);

                    countDownLatch.countDown();

                }

            });

        }

        try {

            countDownLatch.await();

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

        executor.shutdown();

        //等待所有线程统计完成后输出调用次数

        System.out.println("调用次数:"+counterDemo.getCount(url));

    }

}

console output:

调用次数:96526

ConcurrentHashMap 保证的是 单个 get/put/delete 原子,increase 却是 get 再 put 的组合,多线程会互相覆盖。给整个方法加锁又浪费了并发容器。

ConcurrentMap.replace(key, old, new) 才是原子 CAS:

/**

 * Replaces the entry for a key only if currently mapped to a given value.

 * This is equivalent to

 *

 * if (map.containsKey(key) && Objects.equals(map.get(key), oldValue)) {

 *   map.put(key, newValue);

 *   return true;

 * } else

 *   return false;

 * }

 *

 * except that the action is performed atomically.

 */

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

旧值被别人改过,replace 失败;循环里拿最新值再试,就能计准。首次还要用 putIfAbsent 做初始化:

public long increase2(String url) {

        Long oldValue, newValue;

        while (true) {

            oldValue = urlCounter.get(url);

            if (oldValue == null) {

                newValue = 1l;

                //初始化成功,退出循环

                if (urlCounter.putIfAbsent(url, 1l) == null)

                    break;

                //如果初始化失败,说明其他线程已经初始化过了

            } else {

                newValue = oldValue + 1;

                //+1成功,退出循环

                if (urlCounter.replace(url, oldValue, newValue))

                    break;

                //如果+1失败,说明其他线程已经修改过了旧值

            }

        }

        return newValue;

    }

console output:

调用次数:100000
/** If the specified key is not already associated

     * with a value, associate it with the given value.

     * This is equivalent to

     *

     * if (!map.containsKey(key))

     *   return map.put(key, value);

     * else

     *   return map.get(key);

     * }

value 不能为 null。也可以换成 AtomicLong / AtomicLongMap,计数更直:

private AtomicLongMap<String> urlCounter3 = AtomicLongMap.create();

public long increase3(String url) {

    long newValue = urlCounter3.incrementAndGet(url);

    return newValue;

}

public Long getCount3(String url) {

    return urlCounter3.get(url);

java.util.concurrent.atomic 把常用类型包一层原子方法。参考:乐观锁与悲观锁、ConcurrentHashMap、CHM 总结、并发实践。

一句话总结:CAS 用「比较再交换」在硬件上完成无锁更新;ConcurrentHashMap 保证的是单次操作线程安全,组合更新必须自己用 replace/putIfAbsent 自旋,或直接上 AtomicLongMap。

转载请注明来源:ConcurrentHashMap与CAS
本文链接地址:https://ai.zhousir.top/?p=2086
回复 取消