1. 程式人生 > >生產中遇到的Java多執行緒問題

生產中遇到的Java多執行緒問題

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/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物件,打印出物件:[email protected]]

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物件,打印出物件:[email protected]]

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");

    }