转载自 Java 并发实践 — ConcurrentHashMap 与 CAS

最近在做接口限流时涉及到了一个有意思问题,牵扯出了关于concurrentHashMap的一些用法,以及CAS的一些概念。限流算法很多,我主要就以最简单的计数器法来做引。先抽象化一下需求:统计每个接口访问的次数。一个接口对应一个url,也就是一个字符串,每调用一次对其进行加一处理。可能出现的问题主要有三个:

  1. 多线程访问,需要选择合适的并发容器
  2. 分布式下多个实例统计接口流量需要共享内存
  3. 流量统计应该尽可能不损耗服务器性能

但这次的博客并不是想描述怎么去实现接口限流,而是主要想描述一下遇到的问题,所以,第二点暂时不考虑,即不使用Redis。

说到并发的字符串统计,立即让人联想到的数据结构便是ConcurrentHashpMap<String,Long> urlCounter;
如果你刚刚接触并发可能会写出如代码清单1的代码

代码清单1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
publicclass CounterDemo1 {
    privatefinal Map<String, Long> urlCounter = newConcurrentHashMap<>();
    //接口调用次数+1
    publiclong increase(String url) {
        Long oldValue = urlCounter.get(url);
        Long newValue = (oldValue == null) ? 1L : oldValue + 1;
        urlCounter.put(url, newValue);
        returnnewValue;
    }
    //获取调用次数
    publicLong getCount(String url){
        returnurlCounter.get(url);
    }
    publicstatic void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(10);
        finalCounterDemo1 counterDemo = newCounterDemo1();
        intcallTime = 100000;
        finalString url = "http://localhost:8080/hello";
        CountDownLatch countDownLatch = newCountDownLatch(callTime);
        //模拟并发情况下的接口调用统计
        for(inti=0;i<callTime;i++){
            executor.execute(newRunnable() {
                @Override
                publicvoid 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是个线程安全的并发容器,所以没有显示加同步,实际效果呢并不如所愿。

问题就出在increase方法,concurrentHashMap能保证的是每一个操作(put,get,delete…)本身是线程安全的,但是我们的increase方法,对concurrentHashMap的操作是一个组合,先get再put,所以多个线程的操作出现了覆盖。如果对整个increase方法加锁,那么又违背了我们使用并发容器的初衷,因为锁的开销很大。我们有没有方法改善统计方法呢?
代码清单2罗列了concurrentHashMap父接口concurrentMap的一个非常有用但是又常常被忽略的方法。

代码清单2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * Replaces the entry for a key only if currently mapped to a given value.
 * This is equivalent to
 *  <pre> {@code
 * if (map.containsKey(key) && Objects.equals(map.get(key), oldValue)) {
 *   map.put(key, newValue);
 *   return true;
 * } else
 *   return false;
 * }</pre>
 *
 * except that the action is performed atomically.
 */
booleanreplace(K key, V oldValue, V newValue);

这其实就是一个最典型的CAS操作,except that the action is performed atomically.这句话真是帮了大忙,我们可以保证比较和设置是一个原子操作,当A线程尝试在increase时,旧值被修改的话就回导致replace失效,而我们只需要用一个循环,不断获取最新值,直到成功replace一次,即可完成统计。

改进后的increase方法如下

代码清单3:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
publiclong 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失败,说明其他线程已经修改过了旧值
            }
        }
        returnnewValue;
    }
console output:
调用次数:100000

再次调用后获得了正确的结果,上述方案看上去比较繁琐,因为第一次调用时需要进行一次初始化,所以多了一个判断,也用到了另一个CAS操作putIfAbsent,他的源代码描述如下:

代码清单4:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
     * If the specified key is not already associated
     * with a value, associate it with the given value.
     * This is equivalent to
     *  <pre> {@code
     * if (!map.containsKey(key))
     *   return map.put(key, value);
     * else
     *   return map.get(key);
     * }</pre>
     *
     * except that the action is performed atomically.
     *
     * @implNote This implementation intentionally re-abstracts the
     * inappropriate default provided in {@code Map}.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with the specified key, or
     *         {@code null} if there was no mapping for the key.
     *         (A {@code null} return can also indicate that the map
     *         previously associated {@code null} with the key,
     *         if the implementation supports null values.)
     * @throws UnsupportedOperationException if the {@code put} operation
     *         is not supported by this map
     * @throws ClassCastException if the class of the specified key or value
     *         prevents it from being stored in this map
     * @throws NullPointerException if the specified key or value is null,
     *         and this map does not permit null keys or values
     * @throws IllegalArgumentException if some property of the specified key
     *         or value prevents it from being stored in this map
     */
     V putIfAbsent(K key, V value);

简单翻译如下:“如果(调用该方法时)key-value 已经存在,则返回那个 value 值。如果调用时 map 里没有找到 key 的 mapping,返回一个 null 值”。值得注意点的一点就是concurrentHashMap的value是不能存在null值的。实际上呢,上述的方案也可以把Long替换成AtomicLong,可以简化实现, ConcurrentHashMap

1
2
3
4
5
6
7
8
9
10
11
privateAtomicLongMap<String> urlCounter3 = AtomicLongMap.create();
publiclong increase3(String url) {
    longnewValue = urlCounter3.incrementAndGet(url);
    returnnewValue;
}
publicLong getCount3(String url) {
    returnurlCounter3.get(url);
}

看一下他的源码就会发现,其实和代码清单3思路差不多,只不过功能更完善了一点。

和CAS很像的操作,我之前的博客中提到过数据库的乐观锁,用version字段来进行并发控制,其实也是一种compare and swap的思想。

杂谈:网上很多对ConcurrentHashMap的介绍,众所周知,这是一个用分段锁实现的一个线程安全的map容器,但是真正对他的使用场景有介绍的少之又少。面试中能知道这个容器的人也确实不少,问出去,也就回答一个分段锁就没有下文了,但我觉得吧,有时候一知半解反而会比不知道更可怕。

参考:

[1] https://my.oschina.net/mononite/blog/144329
[2] http://www.tuicool.com/articles/zuui6z

Java 并发实践 — ConcurrentHashMap 与 CAS相关推荐

  1. java并发初探ConcurrentHashMap

    java并发初探ConcurrentHashMap Doug Lea在java并发上创造了不可磨灭的功劳,ConcurrentHashMap体现这位大师的非凡能力. 1.8中ConcurrentHas ...

  2. Java并发编程-无锁CAS与Unsafe类及其并发包Atomic

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772470 出自[zejian ...

  3. 【Java并发】-- ConcurrentHashMap如何实现高效地线程安全(jdk1.8)

    文章目录 1.传统集合框架并发编程中Map存在的问题? 2.早期改进策略 3.ConcurrentHashMap采取了哪些方法来提高并发表现(jdk1.8)? 4.ConcurrentHashMap实 ...

  4. Java并发编程-ConcurrentHashMap

    目录 1. JDK 7 HashMap 并发死链 1.1.HashMap回顾 1.2.测试代码 1.3.死链复现 1.4.源码复现 1.5.小结 2. JDK 8 ConcurrentHashMap ...

  5. JAVA并发容器-ConcurrentHashMap 1.7和1.8 源码解析

    HashMap是一个线程不安全的类,在并发情况下会产生很多问题,详情可以参考HashMap 源码解析:HashTable是线程安全的类,但是它使用的是synchronized来保证线程安全,线程竞争激 ...

  6. Java并发编程——ConcurrentHashMap详解

    引出 场景:针对用户来做一个访问次数的记录. 通过HashMap进行记录,key为用户名,value为访问次数. public class ConcurrentHashMapDemo {private ...

  7. Java并发(六)——CAS、AQS、Lock、通信工具类

    文章目录 CAS.AQS.Lock.通信工具类 1 CAS 1.1 Unsafe类 1.2 Atomic包 2 AQS 3 Condition 4 ReentrantLock 4.1 公平锁部分源码 ...

  8. 【Java并发】ConcurrentHashMap原理

    HashTale与ConcurrentHashMap Hashtable与ConcurrentHashMap都是线程安全的集合 Hashtable并发度低,整个Hashtable对应一把锁,同一时刻, ...

  9. java 并发问题存在的原因 解决方案

    java 并发问题存在的原因 & 解决方案 基于jdk1.8 参考<深入理解JVM> <java并发实践> <Linux内核设计与实现>等 并发存在的原因 ...

最新文章

  1. docker oracle navicat_拥抱开源从零开始 Docker、Mysql amp; JPA
  2. Spring Boot 中的 RestTemplate 不好用?试试 Retrofit!
  3. spring,Whitelabel Error Page,This application has no explicit mapping for /error, so you are seeing
  4. token拦截器android_vue.js添加拦截器,实现token认证(使用axios)
  5. java设计模式-模板方法模式
  6. 龙芯mips64 Javajdk下载
  7. plsql破解的办法
  8. CM android rom,华为5X CM 12.1 Android ROM刷机包下载安装教程
  9. 人体的神经系统图 分布,人的神经系统分布图
  10. 万兆交换机用什么网线_千兆网线和万兆网线有什么区别
  11. iOS从零开始学习socket编程——HTTP1.0客户端
  12. 计算机组成原理速成课程【速成】
  13. rippled 02 rippled api 协议使用
  14. 星空银河html,[内蒙好星空]5个夜晚一人逛银河[有星云星系]
  15. npoi 设定视图为分页预览_NPOI导出EXCEL 打印设置分页及打印标题
  16. Java入门 技术总结
  17. 免费视频教程!零基础学Python系列(7) - 数据类型之bytes(上)
  18. greenplum-执行SQL创建SliceGang 学习计划。
  19. 永嘉县公安退休干部李建初诗词创作助力正能量
  20. 这 5 个 APP 开源了!

热门文章

  1. 转 android anr 分析示例,[摘]Android ANR日志分析指南之实例解析
  2. [Java]Java中的i++不是原子操作
  3. [召集令]-Dijkstra的单源最短路径算法
  4. Queue(队列 C++模版实现)
  5. java程序员选择多个offer时需要看重哪些?_对不起,我们公司不要本科以下的大学生,学历对于程序员重不重要...
  6. 算法竞赛进阶指南——后缀数组
  7. SP22343 NORMA2 - Norma(分治优化复杂度)
  8. Codeforces Round #507 (Div. 1) D. You Are Given a Tree 根号分治 + dp
  9. 【POI2011】LIZ-Lollipop 【构造】
  10. hdu 7111-Remove