1. 程式人生 > >Java並發編程原理與實戰八:產生線程安全性問題原因(javap字節碼分析)

Java並發編程原理與實戰八:產生線程安全性問題原因(javap字節碼分析)

cpu next() 讀者 setting pack obj http chm val

前面我們說到多線程帶來的風險,其中一個很重要的就是安全性,因為其重要性因此,放到本章來進行講解,那麽線程安全性問題產生的原因,我們這節將從底層字節碼來進行分析。

一、問題引出

先看一段代碼

package com.roocon.thread.t3;

public class Sequence {
    private int value;

    public int getNext(){
        return value++;
    }

    public static void main(String[] args) {
        Sequence sequence 
= new Sequence(); new Thread(new Runnable() { @Override public void run() { while (true){ System.out.println(Thread.currentThread().getName()+" "+sequence.getNext()); try { Thread.sleep(
100); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); new Thread(new Runnable() { @Override public void run() { while (true
){ System.out.println(Thread.currentThread().getName()+" "+sequence.getNext()); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); new Thread(new Runnable() { @Override public void run() { while (true){ System.out.println(Thread.currentThread().getName()+" "+sequence.getNext()); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }

運行結果:仔細發現,出現了兩個84,但代碼想要的結果是,每個線程每次執行,就在原來的基礎上加一。因此,這裏就是線程的安全問題。

Thread-0 0
Thread-1 1
Thread-2 2
...
Thread-2 81
Thread-1 82
Thread-0 83
Thread-2 84
Thread-1 84
Thread-0 85
Thread-2 86

解釋原因:

return value++; 通過字節碼分析,它其實不是原子操作,value = value + 1;首先,要先讀取value的值,然後再對value的值加1,最後將value+1後的結果賦值給原來的value。

如果有線程1和線程2,假設value此時為83。

1.線程1讀取value的值,為83。

2.線程1對value進行加1操作,得到值是84,但此時cpu被線程2搶走了,線程2還沒來得及將計算後的值賦值給原來的value。

3.線程2讀取value的值,仍然為83。

4.線程2對value進行加1操作,得到84,此時cpu被線程1搶走了,線程1繼續執行賦值操作,將它計算得到的結果值84賦值給value,於是,線程1輸出了84。

5.線程2此時再次搶到了cpu執行權,於是,將它計算得到的結果值84賦值給value,最後輸出84。

下面來查看字節碼文件驗證:

技術分享圖片

繼續往下查看字節碼文件的getNext方法:

技術分享圖片

這些指令告訴我們,value++並不是原子操作。其中,getfield就代表讀取value這個字段的值,iadd就表示對value值進行加1操作,而putfield就代表將jia1操作得到的值賦值給原來的value。

指令的含義可以查看:https://www.cnblogs.com/dougest/p/7067710.html

二、解決問題

那麽,如何解決上面的問題呢?如何保證多線程的安全性問題呢?

最簡單的辦法就是,加同步鎖。

package com.roocon.thread.t3;

public class Sequence {
    private int value;

    public synchronized int getNext(){
        return value++;
    }

    public static void main(String[] args) {
        Sequence sequence = new Sequence();
        new Thread(new Runnable() {
            @Override
            public void run() {
               while (true){
                   System.out.println(Thread.currentThread().getName()+" "+sequence.getNext());
                   try {
                       Thread.sleep(100);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
               }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println(Thread.currentThread().getName()+" "+sequence.getNext());
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println(Thread.currentThread().getName()+" "+sequence.getNext());
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

運行結果:

Thread-0 0
Thread-1 1
Thread-2 2
...
Thread-0 81
Thread-1 82
Thread-2 83
Thread-0 84
Thread-1 85
Thread-2 86
Thread-0 87

解決線程安全性問題有很多解決方案,因為,如果所有的解決方案都是加同步鎖,那麽,所謂的多線程並發最後變成了串行了。那麽,多線程就顯得沒意義了。

最後,總結下產生線程安全性問題三個條件:

1.多線程環境下。

2.多個線程共享一個資源。如servlet就不是線程安全的。在它的service方法中操作同一個實例變量,如果多個線程同時訪問,由於多個線程共享該變量,因此存在線程安全問題。

3.對線程進行非原子性操作。

三、javap的理解

也許我們很少會使用到javap工具,因為現在有很多好的反編譯工具,但是我在此介紹這個工具不是使用它進行反編譯,而是查看java編譯器為我們生成 的字節碼,通過比較字節碼和源代碼,我們可以發現很多的問題,一個很重要的作用就是了解很多編譯器內部的工作機制。

public class Main {
 
 
    public static void main(String[] args) {
        String s = "abc";
        String ss = "ok"+s+"xyz"+5;
        System.out.println(ss);
    }
}

在反編譯前你當然需要先編譯這個類了:javac -g Main.java(使用-g參數是因為要得到下面javap -l時的輸出需要使用此選項)
編譯完成後,我們在使用不同的選項看看不同的效果:

1.先看看最簡單的不帶參數的情況:javap Main:

技術分享圖片

不帶參數的情況將打印類的public信息,包括成員和方法
從上面的輸出中我們確定了兩個知識:如果類沒有顯示的從其它類派生那麽它就是從Object派生;如果沒有為類顯示的申明構造方法,那麽編譯器將為之生成一個缺省構造方法(不帶參數的構造方法)

2.javap -c Main

技術分享圖片

前面的和不帶參數的輸出一樣,後面的顯示了方法的具體的字節碼,從這個輸出裏面我們又可以了解更多的內容.

從上面的代碼很容易看出,雖然在源程序中使用了"+",但在編譯時仍然將"+"轉換成StringBuilder。因此,我們可以得出結論,在Java中無論使用何種方式進行字符串連接,實際上都使用的是StringBuilder類。

3.javap -l Main

技術分享圖片

-l參數將顯示行號和局部變量表

4.javap -p Main

技術分享圖片

-p參數將額外的打印public成員和方法的信息,因為這個類沒有因此輸出相同

這幾個參數幾乎就可以構成javap的最常使用的集合,最常用的應該還是-c選項,因為可以打印字節碼的信息,關於這些字節碼的詳細涵義在Java 虛擬機規範中定義,感興趣的可以查看相關的信息!

5.javap -s Main

技術分享圖片

輸出內部類型簽名

6.javap -v Main

技術分享圖片

輸出棧大小,方法參數的個數

四、為eclipse配置javap命令

javap命令經常使用來對java類文件來進行反編譯,主要用來對java進行分析的工具,在學習Thinking in Java時,因為須要對類文件反編譯。以查看jvm究竟對我們寫的代碼做了哪些優化和處理,比方我看的

使用+=對字符串進行拼接時。jvm的處理方式。

廢話不多說。以下直接帶上配置的教程:

點擊菜單條 Run ---> External tools ---> External tools Configurations... 然後例如以下圖點擊New

技術分享圖片

輸入:

Name: javap

Locations: 選擇jdk的javap.exe文件所在的位置

Working Directory: ${workspace_loc}/${project_name}

Arguments: -classpath bin -c ${java_type_name}

說明:${workspace_loc}表示工作空間所在的路徑;

${project_name}表示項目的名稱;

${java_type_name}表示所選java文件的類名(全名);

上面的這些變量能夠通過每一欄右下方的Variablesbutton去選擇。

(關於其它的一些變量讀者能夠自行去了解)

Arguments的內容: -classpath表示javap命名搜索的類路徑(bin表示是相對於項目的相對路徑) -c表示這裏將生成JVM字節碼

例如以下圖:

技術分享圖片

然後點擊Run, 可能會出現例如以下的錯誤:

技術分享圖片

出現上面那個錯誤,說明你未選中java文件。然後選擇一個java文件。點擊javap,查看反編譯後的結果。順便說一下,你們可能不知道配置後的javap命令去那兒點擊,看下圖就知道去那兒點擊javap了:

技術分享圖片

五、為Idea中添加javap命令

如果將javap命令添加到編譯器中查看字節碼文件會方便很多,下面介紹如何在idea中添加javap命令:

(1)打開setting菜單,

技術分享圖片

(2)找到工具中的擴展工具點擊打開,

技術分享圖片

(3)點擊左側區域左上角的綠色加號按鈕會彈出如下圖這樣的一個編輯框,按提示輸入,

技術分享圖片

(4)完成後點擊ok,點擊setting窗口的apply然後ok,到這裏就已經完成了javap命令的添加,

(5)查看已添加的命令並運行:在代碼編輯區右鍵external tool的擴展選項裏可以看到剛才添加的命令,點擊執行即可。

技術分享圖片

參考資料:

龍果學院 《java並發編程與實戰》

Java並發編程原理與實戰八:產生線程安全性問題原因(javap字節碼分析)