生产中遇到的Java多线程问题

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. 问题解决:找到了问题点,那么解决起来就比较容易了,想必应该有多种方式可以解决这个问题。以下是相对简单的解决方式:

  • 将synchronized关键字放到init()方法上,这样同时只能有一个线程进入到该方法,从而就能避免该问题;
public static synchronized TestSingleton init() {

        if (testSingleton == null) {

            testSingleton = new TestSingleton();

            //创建map

            idCard = new HashMap<>();

            idCard.put("aa", "bb");

        }

        return testSingleton;

    }
  • 将idCard的实例化并赋值的操作放入到构造方法里,在实例化单例对象的同时也实例化了Map对象; 
private TestSingleton() {

        idCard = new HashMap<>();

        idCard.put("aa", "bb");

    }

 

 

 

你可能感兴趣的:(多线程)