HashMap真的线程不安全吗?(告诉你答案)

目录

一、故事是这样开始的

二、复现问题

三、问题原因

四、解决办法


一、故事是这样开始的

测试提了一个bug,根据测试提供的日志定位到对象调用某个方法时显示对象为null,从而导致空指针异常,我一开始百思不得其解,这个对象怎么可能为null?

日志如下,可以看出Retrofit这个对象在调用ceate( )方法时为null

代码是下面这样的,我们来细细品位~

public class HttpUtils {

    ......
    private Map<Integer, Retrofit> retrofitMap = new HashMap<>();
    public <T> T create(Integer key, final Class<T> service) {
        if (!retrofitMap.containsKey(key)) {
            String baseUrl = "";
            ......
            ......
            Retrofit build = new Retrofit.Builder()
                    .baseUrl(baseUrl)
                    .client(okHttpClient)
                    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                    .addConverterFactory(GsonConverterFactory.create())
                    .addConverterFactory(GsonConverterFactory.create(new GsonBuilder()
                            .setDateFormat("yyyy-MM-dd HH:mm:ss")
                            .create())).build();
            retrofitMap.put(key, build);
        }
        return retrofitMap.get(key).create(service);
    }
    ......

}

结合日志和代码定位到代码的第20行,retrofitMap.get(key)为null,导致调用create()方法报空指针异常。那么根据代码来看,有两种可能:

        第一种可能就是put的时后build对象为null,所以get出来为null;

        第二种情况就是build对象不为null,但是put失败了。

后续在17行和18行之间增加日志,打印的日志显示build对象不为null,同时发现这个问题出现的概率为20%左右,所以怀疑可能是多线程导致HashMap在put数据时出现问题,之前听过HashMap线程不安全,但是实际中没有遇到过。这次有机会就仔细探究一下...

二、复现问题

既然是概率事件,那么就可以写一个简单的demo来模拟多线程场景,复现一下问题

TestHashMap4.test();
public class TestHashMap4 {
    private static final String TAG = "Integer";
    private static final Integer KEY[] = {1, 2, 3};
    private Map<Integer, String> map = new HashMap<>();

    /**
     * 入口
     */
    public static void test() {
        for (int i = 0; i < 100; i++) {
            TestHashMap4 testHashMap4 = new TestHashMap4();
            testHashMap4.once(i);
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void once(int count) {
        Log.d(TAG, "----------------------------" + count);
        newThread(KEY[0], KEY[0] + "");
        newThread(KEY[1], KEY[1] + "");
        newThread(KEY[2], KEY[2] + "");
    }

    private void newThread(Integer k, String v) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                dataOpt(k, v);
            }
        }).start();
    }

    private void dataOpt(Integer k, String v) {
        String name = Thread.currentThread().getName();
        Log.d(TAG, "T=" + name + "->" + "put key=" + k + ",value=" + v);
        map.put(k, v);

        String value = map.get(k);
        Log.d(TAG, "T=" + name + "->" + "get key=" + k + ",value=" + value);
    }
}

输出log如下:

2022-03-06 19:30:28.845 26410-26410/com.ecarx.automaptest D/Integer: ----------------------------14
2022-03-06 19:30:28.848 26410-26539/com.ecarx.automaptest D/Integer: T=Thread-46->put key=1,value=1
2022-03-06 19:30:28.851 26410-26539/com.ecarx.automaptest D/Integer: T=Thread-46->get key=1,value=1
2022-03-06 19:30:28.861 26410-26540/com.ecarx.automaptest D/Integer: T=Thread-47->put key=2,value=2
2022-03-06 19:30:28.862 26410-26540/com.ecarx.automaptest D/Integer: T=Thread-47->get key=2,value=2
2022-03-06 19:30:28.864 26410-26541/com.ecarx.automaptest D/Integer: T=Thread-48->put key=3,value=3
2022-03-06 19:30:28.864 26410-26541/com.ecarx.automaptest D/Integer: T=Thread-48->get key=3,value=3
2022-03-06 19:30:29.064 26410-26410/com.ecarx.automaptest D/Integer: ----------------------------15
2022-03-06 19:30:29.067 26410-26544/com.ecarx.automaptest D/Integer: T=Thread-50->put key=2,value=2
2022-03-06 19:30:29.067 26410-26543/com.ecarx.automaptest D/Integer: T=Thread-49->put key=1,value=1
2022-03-06 19:30:29.068 26410-26544/com.ecarx.automaptest D/Integer: T=Thread-50->get key=2,value=2
2022-03-06 19:30:29.068 26410-26543/com.ecarx.automaptest D/Integer: T=Thread-49->get key=1,value=null
2022-03-06 19:30:29.069 26410-26545/com.ecarx.automaptest D/Integer: T=Thread-51->put key=3,value=3
2022-03-06 19:30:29.069 26410-26545/com.ecarx.automaptest D/Integer: T=Thread-51->get key=3,value=3
2022-03-06 19:30:29.270 26410-26410/com.ecarx.automaptest D/Integer: ----------------------------16
2022-03-06 19:30:29.274 26410-26547/com.ecarx.automaptest D/Integer: T=Thread-53->put key=2,value=2
2022-03-06 19:30:29.274 26410-26546/com.ecarx.automaptest D/Integer: T=Thread-52->put key=1,value=1
2022-03-06 19:30:29.274 26410-26546/com.ecarx.automaptest D/Integer: T=Thread-52->get key=1,value=1
2022-03-06 19:30:29.274 26410-26547/com.ecarx.automaptest D/Integer: T=Thread-53->get key=2,value=null
2022-03-06 19:30:29.277 26410-26548/com.ecarx.automaptest D/Integer: T=Thread-54->put key=3,value=3
2022-03-06 19:30:29.277 26410-26548/com.ecarx.automaptest D/Integer: T=Thread-54->get key=3,value=3

哈哈哈。。。真的出现了

从上面的输出的日志,我们看到第12行和19行两处从HashMap中get的值都为null,但是对应的

第10行和第16行put时的值并不为null,这里印证了我们上述猜想(第二种情况)

三、问题原因

其实HashMap的线程不安全主要体现在下面两个方面:
1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。(我属于这种情况)

具体分析过错参考下面这篇问题:

图解HashMap为什么线程不安全?_愿万事胜意-CSDN博客_hashmap为什么线程不安全HashMap的线程不安全主要体现在下面两个方面:1.在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。2.在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。JDK1.7在JDK1.7中,扩容数据时要进行把原数据迁移到新的位置,使用的方法://数据迁移的方法,头插法添加元素void transfer(Entry[] newTable, boolean rehash) { int newCapacity = newTable.length;     https://blog.csdn.net/zzu_seu/article/details/106669757

四、解决办法

我目前使用的解决办法有三种:

第一种:使用专门为多线程并发提供的ConcurrentHashMap集合

//替换前
private Map<Integer, String> map = new HashMap<>();
//替换后
private Map<Integer, String> map = new ConcurrentHashMap<>()

第二种:使用synchronized关键字修饰put操作的整个方法

    //替换前
    private void dataOpt(Integer k, String v) {
        String name = Thread.currentThread().getName();
        Log.d(TAG, "T=" + name + "->" + "put key=" + k + ",value=" + v);
        map.put(k, v);

        String value = map.get(k);
        Log.d(TAG, "T=" + name + "->" + "get key=" + k + ",value=" + value);
    }
    //替换后
    private synchronized void dataOpt(Integer k, String v) {
        String name = Thread.currentThread().getName();
        Log.d(TAG, "T=" + name + "->" + "put key=" + k + ",value=" + v);
        map.put(k, v);

        String value = map.get(k);
        Log.d(TAG, "T=" + name + "->" + "get key=" + k + ",value=" + value);
    }

第三种:使用synchronized关键字修饰put操作的代码块

    //替换前
    private void dataOpt(Integer k, String v) {
        String name = Thread.currentThread().getName();
        Log.d(TAG, "T=" + name + "->" + "put key=" + k + ",value=" + v);
        map.put(k, v);

        String value = map.get(k);
        Log.d(TAG, "T=" + name + "->" + "get key=" + k + ",value=" + value);
    }
    //替换后
    private void dataOpt(Integer k, String v) {
        String name = Thread.currentThread().getName();
        Log.d(TAG, "T=" + name + "->" + "put key=" + k + ",value=" + v);
        synchronized (this) {
            map.put(k, v);
        }

        String value = map.get(k);
        Log.d(TAG, "T=" + name + "->" + "get key=" + k + ",value=" + value);
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值