在做接口限流时涉及到了一个有意思问题,牵扯出了关于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

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是个线程安全的并发容器,所以没有显示加同步,实际效果呢并不如所愿。

问题就出在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.

 */

boolean replace(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

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

再次调用后获得了正确的结果,上述方案看上去比较繁琐,因为第一次调用时需要进行一次初始化,所以多了一个判断,也用到了另一个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

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);

}

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

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

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

如有披露或问题欢迎留言或者入群探讨

ConcurrentHashMap 与 CAS相关推荐

  1. Java 并发实践 — ConcurrentHashMap 与 CAS

    转载自 Java 并发实践 - ConcurrentHashMap 与 CAS 最近在做接口限流时涉及到了一个有意思问题,牵扯出了关于concurrentHashMap的一些用法,以及CAS的一些概念 ...

  2. Java 8 ConcurrentHashMap源码中竟然隐藏着两个BUG

    Java 7的ConcurrenHashMap的源码我建议大家都看看,那个版本的源码就是Java多线程编程的教科书.在Java 7的源码中,作者对悲观锁的使用非常谨慎,大多都转换为自旋锁加volati ...

  3. ConcurrentHashMap 学习笔记

    # ConcurrentHashMap HashMap虽然好用,但是它却不是线程安全的,而在并发度较高的现在,在有些情况下它可能就不是那么合适了,所以需要一个线程安全键值对结构. Hashtable是 ...

  4. 今天轮到我来撕ConcurrentHashMap了

    目录 前言 一.ConcurrentHashMap的实现原理 1. JDK1.7:Segment+HashEntry 2.JDK1.8:synchronized+CAS+红黑树 3.JDK1.7与JD ...

  5. 面试必会系列 - 1.2 Java 集合,源码讲解

    本文已收录至 github,完整图文:https://github.com/HanquanHq/MD-Notes 容器 连老师在公开课里面讲过相关的源码 Collection List CopyOnW ...

  6. java 线程开销_多线程的线程开销

    多线程中两个必要的开销:线程的创建.上下文切换 创建线程: 创建线程使用是直接向系统申请资源的,对操作系统来说,创建一个线程的代价是十分昂贵的, 需要给它分配内存.列入调度,同时在线程切换的时候还要执 ...

  7. HashMap、ConcurretnHashMap面试题详解,源码分析

    文章目录 面试题 HashMap.LinkedHashMap和TreeMap的区别是什么? ①:为什么hashmap每次扩容大小为2的n次方? ③:jdk1.7的hashmap的扩容操作是在元素插入之 ...

  8. 安卓线程同步面试_面试BAT大厂,可少不了这些题目!

    点击上方的终端研发部,右上角选择"设为星标" 每日早8点半,技术文章准时送上 公众号后台回复"学习",获取作者独家秘制精品资料 往期文章 走出腾讯和阿里,大厂员 ...

  9. Java面试题--dalao总结版

    1.UUID:当前日期和时间+时钟序列+全局唯一的IEEE机器识别号(如果有网卡,从网卡MAC地址获取) import java.util.UUID; UUID.randUUID().toString ...

最新文章

  1. UIBezierPath的使用(持续更新)
  2. Android之ActionBar常用设计和使用总结
  3. QTP自传之web常用对象
  4. Phaser开源2d引擎 javascript/html5游戏框架
  5. apache 添加下载文件头
  6. CSS两栏布局之右栏布局
  7. Oracle技术之SYS用户对象不支持延迟段
  8. 基于ARM的SoC设计入门
  9. 基于docker的Mongodb部署文档 文件映射方式
  10. 谷歌软件工程师_这是我曾经在Google担任软件工程师的简历。
  11. 天线匹配与人体之间的关系
  12. 安卓修改Airpods的双击功能,改“播放暂停”————下一首
  13. Image Pro Plus6进行荧光分析与荧光照片合成学习笔记
  14. python编程语言进化_编程语言的进化史和分类
  15. et文件怎么转成excel_excel怎么转换成word格式的文件格式?这些方法应该掌握!...
  16. 如何做一个优秀的团队成员
  17. http服务器常见状态码
  18. 大学计算机基础实践教程实验一讨论与思考,大学计算机基础实验报告手册(非艺术类).doc...
  19. (二)zynq芯片是什么
  20. 使用ajax访问腾讯地图api,腾讯地图ajax获取路线规划结果

热门文章

  1. 山东最新建筑八大员(质量员)机考真题及答案解析
  2. 如何彻底卸载百度卫士?
  3. 教你制作自己的电脑主题包
  4. Linux Mint 18.2安装WPS之优化设置
  5. mysql verycd,山寨精简版veryCD网站 simpleCD震撼上线
  6. 【bat】bat文件修改注册表
  7. windows下安装libxml2
  8. SpiderMonkey相关学习资料
  9. 基于CNN的自动化测试实践
  10. 关于VMware15使用unlocker解锁Mac OS时cmd一闪而过导致失败的问题解决方法