前言

老王为何半夜惨叫?几行代码为何导致服务器爆炸?说好的线程安全为何还是出问题?让我们一起收看今天的《走进IT》

正文

CurrentHashMap出现背景

说到ConcurrentHashMap的出现背景,还得从HashMap说起。

老王是某公司的苦逼Java开发,在互联网行业中,业务总是迭代得非常快。体现在代码中的话,就是v1.0的模块是单线程执行的,这时候使用HashMap是一个不错的选择。然而到了v1.5的版本,为了性能考虑,老王觉得把这段代码改成多线程会更有效率,那么说改就改,然后就愉快的发布上线了。

直到某天晚上,突然收到线上警报,服务器CPU占用100%。这时候惊醒起来一顿排查(百度,谷歌),结果发现原来是HashMap 在并发的环境下进行rehash的时候会造成链表的闭环,因此在进行get()操作的时候导致了CPU占用100%。喔,原来HashMap不是线程安全的类,在当前的业务场景中会有问题。那么你这时候又想到了Hashtable,没错,这是个线程安全的类,那我先用这个类替换不就行了,一顿commit,push,部署上去了,观察了一段时间,完美~再也没出现过类似的问题了。

但是好日子过的并不长久,运营的同事又找上门了,老王啊,XX功能怎么慢了这么多啊?这时候老王就纳闷了,我没改代码啊?不就上次替换了一个Hashtable,难道这里会有效率的问题?然后又是一顿排查(百度、谷歌),我去,果不其然,原来它线程安全的原因是因为在方法上都加了synchronized,导致我们全部操作都串行化了,难怪这么慢。

经过了2次掉陷阱的经验,这次的老王已经是非常谨慎的去寻求更好的解决方案了,这时他找到ConcurrentHashMap,而且为了避免再次掉坑他也去提前了解了实现原理,原来这个类是使用了Segment分段锁,每一个Segment都有自己的锁,这样冲突的的范围就变小了,效率也能提高不少。经过调研发现确实不错,于是他就放心的把Hashtable给替换掉了,从此运营再也没来吐槽了,老王又过上了幸福的日子。

经过一段时间紧张的业务开发,此时的项目已经去到了v2.0,之前的ConcurrentHashMap相关的代码已经被改的面目全非,逻辑也复杂了很多,但项目还是按时顺利的上线了。在项目在运行了一段时间以后,居然再次出现线程安全的问题,其根源竟然是ConcurrentHashMap,老王叕陷入了沉思...

为何会出问题?

抛开复杂的例子,我们用一个多线程并发获取map中的值并加1,看看最后输出的数字如何

public class CHMDemo {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<String,Integer>();
map.put("key", 1);
ExecutorService executorService = Executors.newFixedThreadPool(100);
for (int i = 0; i < 1000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
int key = map.get("key") + 1; //step 1
map.put("key", key);//step 2
}
});
}
Thread.sleep(3000); //模拟等待执行结束
System.out.println("------" + map.get("key") + "------");
executorService.shutdown();
}
}

此时我们看看多次执行输出的结果

------790------
------825------
------875------

  

通过观察输出结果可以发现,这段使用ConcurrentHashMap的代码,产生了线程安全的问题。我们来分析一下为什么会发生这种情况。在step1跟step2中,都只是调用ConcurrentHashMap的方法,各自都是原子操作,是线程安全的。但是他们组合在一起的时候就会有问题了,A线程在进入方法后,通过map.get("key")拿到key的值,刚把这个值读取出来还没有加1的时候,线程B也进来了,那么这导致线程A和线程B拿到的key是一样的。不仅仅是在

ConcurrentHashMap,在其他的线程安全的容器比如Vector之类的也会出现如此情况,所以在使用这些容器的时候还是不能大意。

如何解决?

1、可以用synchronized

synchronized(this){
//step1
//step2
}
但是用这种方法的话,我们要考虑一下效率的问题,会不会对当前的业务影响很大?

2、用原子类

public class CHMDemo {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap<String,AtomicInteger>();
AtomicInteger integer = new AtomicInteger(1);
map.put("key", integer);
ExecutorService executorService = Executors.newFixedThreadPool(100);
for (int i = 0; i < 1000; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
map.get("key").incrementAndGet();
}
});
}
Thread.sleep(3000); //模拟等待执行结束
System.out.println("------" + map.get("key") + "------");
executorService.shutdown();
}
}

输出

------1001------

此时的输出结果就正确了,效率上也比第一种解决方案提高很多。

结语

人生处处是陷阱,写代码也是如此,多思考,多留心。

------------------------------------------------------

【推荐阅读】

大白话搞懂什么是同步/异步/阻塞/非阻塞

Java异常处理最佳实践及陷阱防范

论JVM爆炸的几种姿势及自救方法

解放程序员双手之Supervisor

看完本文有收获?请转发分享给朋友吧

关注「深夜里的程序猿」,分享最干的干货

使用ConcurrentHashMap一定线程安全?的更多相关文章

  1. HashMap(不是线程安全)与ConcurrentHashMap(线程安全)

    HashMap不是线程安全的 ConcurrentHashMap是线程安全的 从JDK1.2起,就有了HashMap,正如前一篇文章所说,HashMap不是线程安全的,因此多线程操作时需要格外小心. ...

  2. 面试连环炮系列(九):为什么ConcurrentHashMap是线程安全的

    为什么ConcurrentHashMap是线程安全的 JDK1.7中,ConcurrentHashMap使用的锁分段技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一 ...

  3. 为什么ConcurrentHashMap是线程安全的?

    ConcurrentHashMap 是 HashMap 的多线程版本,HashMap 在并发操作时会有各种问题,比如死循环问题.数据覆盖等问题.而这些问题,只要使用 ConcurrentHashMap ...

  4. ConcurrentHashMap实现线程安全的原理

    并发环境下为什么使用ConcurrentHashMap 1. HashMap在高并发的环境下,执行put操作会导致HashMap的Entry链表形成环形数据结构,从而导致Entry的next节点始终不 ...

  5. 面试题:ConcurrentHashMap实现线程安全的原理

    在ConcurrentHashMap没有出现以前,jdk使用hashtable来实现线程安全,但是hashtable是将整个hash表锁住,所以效率很低下. ConcurrentHashMap将数据分 ...

  6. ConcurrentHashMap、synchronized与线程安全

    明明用了ConcurrentHashMap,可是始终线程不安全, 下面我们来看代码: public class Test40 { public static void main(String[] ar ...

  7. Java ConcurrentHashMap存入引用对象时也是线程安全的

    本人小白,看到资料说ConcurrentHashMap是线程安全的,get过程不需要加锁,put是线程安全的,推荐高并发时使用.但是本人不清楚是否该map中存入的引用类型对象,对象属性变化也是否线程安 ...

  8. ConcurrentHashMap并不是绝对线程安全的

    import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; publi ...

  9. 【线程】结果缓存实现(future与concurrenthashmap)

    Computable<A,V>接口中生命了一个函数Computable,其输入类型为A,输出类型为V,在ExpensiveFunction中实现的Computable,需要很长时间来计算结 ...

随机推荐

  1. flash builder 4.6与myecilpse 10.7集成

    一.在flash builder 4.0以后就没有单独提供插件版的flash builder了,因此必须先安装完整版的flash builder,再进行插件集成. 二.集成过程比较简单但也有几个要注意 ...

  2. Java多线程:生命周期,实现与调度

    Java线程生命周期 Java线程实现方法 继承Thread类,重写run()方法 实现Runnable接口,便于继承其他类 Callable类替换Runnable类,实现返回值 Future接口对任 ...

  3. Java并发之AQS详解

    一.概述 谈到并发,不得不谈ReentrantLock:而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)! 类如其名,抽象的队列式的同步器,AQ ...

  4. iOS xcode9 framework静态库的创建以及xib和图片的使用记录

    来到了新公司,要开发的第一个项目据说可能要封成framework,可是我从来没自己做过framework呀!顿时开始发愤图强,赶紧恶补了起来.但是还是遇到了一些乱七八糟的情况,所以写个随笔记下来. 1 ...

  5. 读《图解HTTP》有感-(HTTP报文内的HTTP消息)

    写在前面 HTTP通信包括从客户端到服务端的的请求以及服务端返回客户端的响应 正文 1.什么是HTTP报文?它由什么构成?包含几个部分? 用于HTTP协议交互的信息就是HTTP报文:它是由多行数据构成 ...

  6. Linux文本处理命令 -- grep

    简介 grep (global search regular expression(RE) and print out the line,全面搜索正则表达式并把行打印出来)是一种强大的文本搜索工具,它 ...

  7. range与enumerate的区别

    在迭代中enumerate比range更能灵活,一般情况下尽量用erumerate,下面举例说明: 先来看range的使用: city_list = ['beijing', 'shanghai', ' ...

  8. Autopep8的使用

    什么是Autopep8 在python开发中, 大家都知道,python编码规范是PEP8,但是在市级开发中有的公司严格要求PEP8规范开发, 有的公司不会在乎那些,在我的理解中,程序员如果想走的更高 ...

  9. java,maven工程打tar.gz包执行main方法

    一,需要在pom.xml文件添加plugin, 项目目录结构 <build> <plugins> <plugin> <artifactId>maven- ...

  10. java 泛型详解(普通泛型、 通配符、 泛型接口,泛型数组,泛型方法,泛型嵌套)

    JDK1.5 令我们期待很久,可是当他发布的时候却更换版本号为5.0.这说明Java已经有大幅度的变化.本文将讲解JDK5.0支持的新功能-----Java的泛型.  1.Java泛型  其实Java ...