1. 问题场景:前几天一位同事分享了一段代码,这段代码在线上偶尔会报空指针异常,虽然是一个简单的NullPointerException,却是一个很不容易发现的多线程问题导致的,我们一起先来看下代码(这是我复原的测试代码,并非和生产一致,但整体逻辑是一样的):
public class TestSingleton {
private static volatile TestSingleton testSingleton = null;
static Map 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/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/tools.jar:/Users/workspace/ideaSpace/jwt-helper/target/classes:/Users/workspace/mavenRepository/com/auth0/java-jwt/3.3.0/java-jwt-3.3.0.jar:/Users/workspace/mavenRepository/com/fasterxml/jackson/core/jackson-databind/2.9.2/jackson-databind-2.9.2.jar:/Users/workspace/mavenRepository/com/fasterxml/jackson/core/jackson-annotations/2.9.0/jackson-annotations-2.9.0.jar:/Users/workspace/mavenRepository/com/fasterxml/jackson/core/jackson-core/2.9.2/jackson-core-2.9.2.jar:/Users/workspace/mavenRepository/io/jsonwebtoken/jjwt/0.9.0/jjwt-0.9.0.jar:/Users/workspace/mavenRepository/com/alibaba/fastjson/1.2.47/fastjson-1.2.47.jar:/Users/workspace/mavenRepository/org/apache/commons/commons-lang3/3.7/commons-lang3-3.7.jar:/Users/workspace/mavenRepository/org/slf4j/slf4j-log4j12/1.6.4/slf4j-log4j12-1.6.4.jar:/Users/workspace/mavenRepository/log4j/log4j/1.2.16/log4j-1.2.16.jar:/Users/workspace/mavenRepository/org/slf4j/slf4j-api/1.7.20/slf4j-api-1.7.20.jar:/Users/workspace/mavenRepository/commons-codec/commons-codec/1.11/commons-codec-1.11.jar com.chtwm.component.secret.TestSingleton
Exception in thread "pool-1-thread-2" Exception in thread "pool-1-thread-3" java.lang.NullPointerException
at com.chtwm.component.secret.TestSingleton.getIdCard(TestSingleton.java:43)
at com.chtwm.component.secret.TestSingleton.lambda$main$0(TestSingleton.java:52)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
java.lang.NullPointerException
at com.chtwm.component.secret.TestSingleton.getIdCard(TestSingleton.java:43)
at com.chtwm.component.secret.TestSingleton.lambda$main$0(TestSingleton.java:52)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 130 (interrupted by signal 2: SIGINT)
报错位置是getIdCard()方法获取idCard中信息的时候,
public String getIdCard() {
return idCard.get("aa");
}
说明是idCard这个map是Null(一开始我的猜想也是这个map对象可能存在问题,因为除了Map相关的操作代码,其他代码是标准的单例实现模式,我之前也经常使用这样的模式),那么什么情况会导致Map对象为空了呢?
我们再回头捋一下代码,在多线程情况下,有这样的场景会造成idCard对象为Null:
假如有一个线程A进入到了同步锁,且刚执行完
testSingleton = new TestSingleton();
操作,还未进行下一步实例化Map对象。此时,另一个线程B同时在判断testSingleton是否为Null,
if (testSingleton == null)
因为testSingleton对象是被volatile修饰的,volatile修复的变量具有多线程可见性、防指令重排序的特性,所以此时testSingleton是非空的,那么将直接返回testSingleton对象调用getIdCard(),但此时Map对象idCard又还没有实例化,是一个null值,因此在取map对象中key值的时候导致了报出NullPointException。
3. 问题验证:现在你应该大概明白是什么原因了吧,为了再次验证一下我们分析的结果,我把线程数设置成2个,然后在实例化idCard对象前休眠5秒钟,然后再加一些日志打印,更能清晰的说明问题,修改后的代码:
public static TestSingleton init() {
System.out.println("当前线程名称>>" + Thread.currentThread().getName());
if (testSingleton == null) {
synchronized (TestSingleton.class) {
if (testSingleton == null) {
testSingleton = new TestSingleton();
System.out.println(Thread.currentThread().getName() + "[持有锁并实例化完成testSingleton对象==开始休眠...]");
//休眠一会
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "[休眠完成]");
//创建map
idCard = new HashMap<>();
idCard.put("aa", "bb");
}
}
}
System.out.println(Thread.currentThread().getName() + "[返回testSingleton对象,打印出对象:"+testSingleton+"]");
return testSingleton;
}
public String getIdCard() {
System.out.println(Thread.currentThread().getName() + "[从idCard对象获取key值,打印idCard对象:"+idCard+"]");
return idCard.get("aa");
}
设置2个线程并发,再次执行,这样基本每次都会复现问题,以下是错误日志信息(日志打印的顺序可能有点错乱,但不影响关键问题点的分析):
/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/bin/java "-javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=52197:/Applications/IntelliJ IDEA.app/Contents/bin" -Dfile.encoding=UTF-8 -classpath /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/charsets.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/deploy.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/cldrdata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/dnsns.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/jaccess.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/jfxrt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/localedata.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/nashorn.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunec.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/ext/zipfs.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/javaws.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jce.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfr.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jfxswt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/jsse.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/management-agent.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/plugin.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/resources.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/ant-javafx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/dt.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/javafx-mx.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/jconsole.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/packager.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/sa-jdi.jar:/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/lib/tools.jar:/Users/workspace/ideaSpace/jwt-helper/target/classes:/Users/workspace/mavenRepository/com/auth0/java-jwt/3.3.0/java-jwt-3.3.0.jar:/Users/workspace/mavenRepository/com/fasterxml/jackson/core/jackson-databind/2.9.2/jackson-databind-2.9.2.jar:/Users/workspace/mavenRepository/com/fasterxml/jackson/core/jackson-annotations/2.9.0/jackson-annotations-2.9.0.jar:/Users/workspace/mavenRepository/com/fasterxml/jackson/core/jackson-core/2.9.2/jackson-core-2.9.2.jar:/Users/workspace/mavenRepository/io/jsonwebtoken/jjwt/0.9.0/jjwt-0.9.0.jar:/Users/workspace/mavenRepository/com/alibaba/fastjson/1.2.47/fastjson-1.2.47.jar:/Users/workspace/mavenRepository/org/apache/commons/commons-lang3/3.7/commons-lang3-3.7.jar:/Users/workspace/mavenRepository/org/slf4j/slf4j-log4j12/1.6.4/slf4j-log4j12-1.6.4.jar:/Users/workspace/mavenRepository/log4j/log4j/1.2.16/log4j-1.2.16.jar:/Users/workspace/mavenRepository/org/slf4j/slf4j-api/1.7.20/slf4j-api-1.7.20.jar:/Users/workspace/mavenRepository/commons-codec/commons-codec/1.11/commons-codec-1.11.jar com.chtwm.component.secret.TestSingleton
当前线程名称>>pool-1-thread-1
当前线程名称>>pool-1-thread-2
pool-1-thread-1[持有锁并实例化完成testSingleton对象==开始休眠...]
pool-1-thread-2[返回testSingleton对象,打印出对象:com.chtwm.component.secret.TestSingleton@5be66ee5]
Exception in thread "pool-1-thread-2" java.lang.NullPointerException
pool-1-thread-2[从idCard对象获取key值,打印idCard对象:null]
at com.chtwm.component.secret.TestSingleton.getIdCard(TestSingleton.java:54)
at com.chtwm.component.secret.TestSingleton.lambda$main$0(TestSingleton.java:63)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
pool-1-thread-1[休眠完成]
pool-1-thread-1[返回testSingleton对象,打印出对象:com.chtwm.component.secret.TestSingleton@5be66ee5]
pool-1-thread-1[从idCard对象获取key值,打印idCard对象:{aa=bb}]
从错误信息中就可以看出,线程1持有了同步锁并实例化了testSingleton对象,然后开始休眠,idCard并没有实例化。此时线程2直接返回了testSingleton对象,调用了getIdCard()方法,但idCard对象还是null,所以就报出了空指针异常,验证完毕。
4. 问题解决:找到了问题点,那么解决起来就比较容易了,想必应该有多种方式可以解决这个问题。以下是相对简单的解决方式:
public static synchronized TestSingleton init() {
if (testSingleton == null) {
testSingleton = new TestSingleton();
//创建map
idCard = new HashMap<>();
idCard.put("aa", "bb");
}
return testSingleton;
}
private TestSingleton() {
idCard = new HashMap<>();
idCard.put("aa", "bb");
}