目录
一、故事是这样开始的
测试提了一个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操作时会发生数据覆盖的情况。(我属于这种情况)
具体分析过错参考下面这篇问题:
四、解决办法
我目前使用的解决办法有三种:
第一种:使用专门为多线程并发提供的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);
}