使用ConcurrentHashMap必定线程安全?

前言

老王为什么半夜惨叫?几行代码为什么致使服务器爆炸?说好的线程安全为什么仍是出问题?让咱们一块儿收看今天的《走进IT》java

正文

CurrentHashMap出现背景

说到ConcurrentHashMap的出现背景,还得从HashMap提及。程序员

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

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

可是好日子过的并不长久,运营的同事又找上门了,老王啊,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();
    }
}
复制代码

此时咱们看看屡次执行输出的结果ide

------790------
------825------
------875------
复制代码

经过观察输出结果能够发现,这段使用ConcurrentHashMap的代码,产生了线程安全的问题。咱们来分析一下为何会发生这种状况。在step1跟step2中,都只是调用ConcurrentHashMap的方法,各自都是原子操做,是线程安全的。可是他们组合在一块儿的时候就会有问题了,A线程在进入方法后,经过map.get("key")拿到key的值,刚把这个值读取出来尚未加1的时候,线程B也进来了,那么这致使线程A和线程B拿到的key是同样的。不只仅是在post

ConcurrentHashMap,在其余的线程安全的容器好比Vector之类的也会出现如此状况,因此在使用这些容器的时候仍是不能大意。

如何解决?

一、能够用synchronized

synchronized(this){
    //step1
    //step2
}

复制代码

可是用这种方法的话,咱们要考虑一下效率的问题,会不会对当前的业务影响很大?

二、用原子类

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

有收获的话,就点个赞吧

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

相关文章
相关标签/搜索