Java 并发实践 — ConcurrentHashMap 与 CAS

转载自 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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/330067.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

git rebase命令(转)

转自&#xff1a; https://www.yiibai.com/git/git_rebase.html git rebase命令在另一个分支基础之上重新应用&#xff0c;用于把一个分支的修改合并到当前分支。 使用语法 git rebase [-i | --interactive] [options] [--exec <cmd>] [--onto <newbase>][<u…

python tkinter计算器实例_python -Tkinter 实现一个小计算器功能

原博文 2017-03-25 22:08 − 文章来源&#xff1a;http://www.cnblogs.com/Skyyj/p/6618739.html 本代码是基于python 2.7的 如果是对于python3.X 则需要将 tkinter 改为Tkinter 将tkMessagebox&... 相关推荐 2019-12-10 15:59 − python GUI编程(Tkinter) Python 提供了多…

Spring IOC 容器启动、Bean生命周期详解

前言 在Spring IOC 和 AOP 概览中&#xff0c;简要介绍了IOC容器和AOP&#xff0c;没有深入IOC容器Bean的实例化&#xff0c;此文承接上文深入分析Bean的实例化过程、生命周期。 Spring IOC的过程 Spring的IoC容器在实现控制反转和依赖注入的过程中,可以划分为两个阶段: 容…

java 8 Lambda 表达式(副作用)

【1】转自&#xff1a; https://www.cnblogs.com/linlinismine/p/9283532.html 早在2014年oracle发布了jdk 8,在里面增加了lambda模块。于是java程序员们又多了一种新的编程方式&#xff1a;函数式编程&#xff0c;也就是lambda表达式。我自己用lambda表达式也差不多快4年了&am…

Java NIO:浅析I/O模型

转载自 Java NIO&#xff1a;浅析I/O模型也许很多朋友在学习NIO的时候都会感觉有点吃力&#xff0c;对里面的很多概念都感觉不是那么明朗。在进入Java NIO编程之前&#xff0c;我们今天先来讨论一些比较基础的知识&#xff1a;I/O模型。下面本文先从同步和异步的概念 说起&…

ubuntu安装python3.8_Ubuntu 16.04 安装 python3.8

Ubuntu 16.04 amd64 (64bit)&#xff08;纯净版&#xff09; 自带python2.7和python3.5 执行"whereis python"查看当前安装的python [rootroot ~]# whereis python python: /usr/bin/python2.7 /usr/bin/python /usr/lib/python2.7 /usr/lib64/python2.7 /etc/python…

Spring IOC 如何解决循环依赖?

前言 假设对象A、B 之间相互依赖&#xff0c;Spring IOC是如何解决A、B两个对象的实例化的&#xff1f;答案是三级缓存。 三级缓存 SpringIOC 通过三级缓存来解决循环依赖问题&#xff0c;三级缓存指的是三个Map&#xff1a; singletonObjects&#xff1a;一级缓存&#xf…

pythondocx模板_使用python-docx-template修改word文档

由于最近工作中需要自动修改word文档&#xff0c;并生成PDF文件&#xff0c;经过查阅资料后发现使用python-docx-template可以完成对word的修改工作&#xff0c;于是记录一下使用方法。文章内容大部分来自对以下博客的整理和学习https://blog.csdn.net/weixin_42670653/article…

面试必问的 CAS ,要多了解

转载自 面试必问的 CAS &#xff0c;要多了解前言 CAS&#xff08;Compare and Swap&#xff09;&#xff0c;即比较并替换&#xff0c;实现并发算法时常用到的一种技术&#xff0c;Doug lea大神在java同步器中大量使用了CAS技术&#xff0c;鬼斧神工的实现了多线程执行的安全性…

MySQL 对于千万级的大表要怎么优化?

很多人第一反应是各种切分&#xff1b; 我给的顺序是: 第一 优化你的sql和索引&#xff1b; 第二 加缓存&#xff0c;memcached,redis&#xff1b; 第三 以上都做了后&#xff0c;还是慢&#xff0c;就做主从复制或主主复制&#xff0c;读写分离&#xff0c;可以在应用层做&…

MySQL元数据库——information_schema

转自&#xff1a; https://www.cnblogs.com/postnull/p/6697077.html 平时使用MySQL客户端操作数据库的同学&#xff0c;只要稍微留神都会发现&#xff0c;除了我们建的库之外&#xff0c;还经常看到三个数据库的影子&#xff1a; 1. information_schema 2. performance_sche…

mysql 表字段信息从一张表迁移到另一张表_MySQL(数据库)笔记

###数据库之前通过流去操作文件保存数据库的弊端:1.执行效率低2.开发成本高3.一般只能保存小量数据4.只能保存文本数据####什么是DB- DataBase 数据库:代表文件集合####什么是DBMS- DataBaseManagementSystem 数据库管理系统(软件),用于管理保存数据的文件集合,用于和程序员进行…

GET与POST传递数据的最大长度能够达到多少

各种web开发语言中&#xff0c;各个页面之间基本都会进行数据的传递&#xff0c;web开发里面比较常用的数据传递方式有get post&#xff0c;一直以来我都只知道get传递的数据量要比post传递的数据量要少&#xff0c;所以传递大数据量还是要用post&#xff0c;但是 get post 这两…

maven命令实战

【1】 创建maven项目 1&#xff09;目录结构 mavenhello09|---src|---|---main|---|---|---java|---|---|---resources|---|---test|---|---|---java|---|---|---resources|---pom.xml 目录结构说明&#xff1a; main/java&#xff1a;主程序&#xff1b;main/resources&…

Mac 环境变量配置

环境变量配置 cd ~ (回到主目录home)如果你是第一次配置环境变量&#xff0c;可以使用“touch .bash_profile” 创建一个.bash_profile的隐藏配置文件vim .bash_profile&#xff0c;写入相应的环境变量&#xff0c;如下&#xff1a; # golang配置 export GOROOT/usr/local/Ce…

python测试开发面试题_python测试开发面试之深浅拷贝

先来道题热热身 a (a, b,c) c copy.copy(a) d copy.deepcopy(a) if c d: print("c和d的值相等") if id(c) id(d): print("c和d的地址相等") 想想最后打印的是什么&#xff1f; 什么是深拷贝和浅拷贝 深拷贝&#xff0c;就是在对某个对象进行拷贝的时候…

linux虚拟机tomcat上部署web项目的常用命令

1&#xff09;查看 tomcat是否在运行 ps -ef | grep tomcat ps -ef 补充&#xff1a;Linux中的ps命令是Process Status的缩写&#xff0c;ps命令用来列出系统中当前运行的那些进程。ps命令可以列出当前进程的运行情况&#xff08;状态、时间等信息&#xff09;。在Linux系统中…

一文理清Cookie、Session、Token

发展史 1、很久很久以前&#xff0c;Web 基本上就是文档的浏览而已&#xff0c; 既然是浏览&#xff0c;作为服务器&#xff0c; 不需要记录谁在某一段时间里都浏览了什么文档&#xff0c;每次请求都是一个新的HTTP协议&#xff0c; 就是请求加响应&#xff0c; 尤其是我不用记…

python中debug有什么用途_史上最方便的Python Debug工具

最近在github上冒出了一个python的debug神器PySnooper&#xff0c;号称在debug时可以消灭print。那么该工具有哪些优点呢&#xff0c;如何使用该工具呢。本文就介绍该工具的优缺点和使用方式。 前言 使用python开发过程中&#xff0c;总是避免不了debug。传统的debug过程大致分…

能力陷阱总结

【readme】 本文总结于《能力陷阱》&#xff0c;感觉非常不错&#xff0c;有兴趣的朋友可以看下&#xff1b; 【1】领导者的能力陷阱 1&#xff09;改变思想从行动开始&#xff1b; 改变做事方式&#xff0c;然后才能改变自己的思考方式&#xff1b; 2&#xff09;要先在行…