1. 问题场景:前几天一位同事分享了一段代码,这段代码在线上偶尔会报空指针异常,虽然是一个简单的NullPointerException,却是一个很不容易发现的多线程问题导致的,我们一起先来看下代码(这是我复原的测试代码,并非和生产一致,但整体逻辑是一样的):
public class TestSingleton {
private static volatile TestSingleton testSingleton = null;
static Map<String, String> idCard;
private TestSingleton() {}
public static TestSingleton init() {
if (testSingleton == null) {
synchronized (TestSingleton.class) {
if (testSingleton == null) {
testSingleton = new TestSingleton();
idCard = new HashMap<>();
idCard.put("aa", "bb");
}
}
}
return testSingleton;
}
public String getIdCard() {
return idCard.get("aa");
}
public static void main(String[] args) {
TestSingleton testSingleton = TestSingleton.init().getIdCard();
}
}
整体来看这位仁兄是实现了一个单例类,看样子没什么问题,只是多了一个Map来实例化并设置一些kv值,从逻辑上来没看出有什么毛病,我一开始看到这个代码确实没看出什么问题来。
如果先不看后续的描述,你能看出问题来吗?
静静地观察一分钟...
2. 问题还原分析:通过观察看出是什么问题了吗?如果你立马定位了原因,那么说明你的多线程还是挺扎实的,接下来我们一块来分析一下问题原因。首先我启动main方法执行,没有发现任何问题,多试几次也没问题,但这并不能说明就真正没问题(要不然生产上的问题就没法解释了),因为我现在这样执行main方法是单线程,只能说明这个代码在单线程场景下是没有任何问题的。现在我将main方法改成多线执行:
public static void main(String[] args) {
int n = 10;
CountDownLatch countDownLatch = new CountDownLatch(n);
ExecutorService executorService = Executors.newFixedThreadPool(n);
for (int i = 0; i < n; i++) {
executorService.execute(() -> {
TestSingleton.init().getIdCard();
countDownLatch.countDown();
});
}
try {
//等待所有线程执行完毕
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
//统计一下执行情况,看是否都执行完成
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
System.out.println("总任务数:" + threadPoolExecutor.getTaskCount());
System.out.println("正在执行任务数字:" + threadPoolExecutor.getActiveCount());
System.out.println("完成任务数:" + threadPoolExecutor.getCompletedTaskCount());
}
接下来我使用线程池,启动10个线程试试看能否还原出报NullPointException,多执行几次发现确实报出了空指针,
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=52108:/Applications/IntelliJ IDEA.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/charse