Java併發編程之線程知識五:線程安全

目錄

1.基礎概念

2.造成線程不安全的條件 

3.變量在JVM內存中的存儲

4.變量種類與線程安全

5.如何避免線程不安全


1.基礎概念

  • 線程:線程是程序中一個單一的順序控制流程,在單個程序中同時運行多個線程完成不同的工作,稱爲多線程。
    例如:在電子商務網站中用戶發起一個請求,服務器從收到這條請求開始到處理完所有的業務邏輯返回結果的過程一般就是一個線程。當客戶端N多的請求同時請求服務器,這就是多線程併發。
  • 共享資源:允許被不同線程訪問的資源,共享資源是多線程中允許被不只一個線程訪問的類變量或實例變量,共享資源可以是單個類變量或實例變量,也可以是一組類變量或實例變量。
  • 鎖:當有多個線程共用一共享資源的時候,便會出現資源爭搶(衝突),鎖就是用來解決這種衝突,保證各個線程有序使用資源的。這個解決衝突的過程跟上廁所一樣,假如有ABC三個人都來上廁所而廁所只有一個坑,一次只能進一人,A先來了,那麼在A出來之前,這個廁所就處在了“鎖”定狀態,B和C憋死也要在外面等着,直到A出門(原因很多,如睡着了,方便完了,忘帶廁紙了跑出來找人要....)“鎖”定解除B和C才能進入。
  • 線程安全:一個資源在可以被多個線程中的對象調用情況下,不會出現任何衝突的時候就是線程安全的。

2.造成線程不安全的條件 

一個線程操作共享資源的過程如下: 
將共享資源從主內存拷貝副本到工作內存==>對該副本進行修改操作==>將該副本從工作內存寫回到主內存,這樣就可能出現在兩個線程同時將主內存中的共享資源副本拷貝到各自的工作內存,但是兩個線程在將自己修改後的副本放回主內存的時候就會有先後問題,後放回去的就會覆蓋先放回去的內容這就造成了線程不安全。
造成線程不安全的唯一條件就是“存在多個線程同時修改一個共享資源”。
其中包括下面兩個關鍵點:
(1)存在共享資源
(2)不同線程同時對共享資源修改

3.變量在JVM內存中的存儲

4.變量種類與線程安全

4.1.靜態變量

靜態變量即類變量,位於JVM內存的方法區,爲所有對象共享,一旦靜態變量被修改,其他對象均對修改可見,故靜態變量是線程不安全的。

4.1.1.靜態變量線程不安全測試代碼

4.1.1.1StaticVariableTest.java代碼
public class StaticVariableTest extends Thread {
    /**
     * 靜態變量(共享資源)
     */
    private static int static_i;

    public void run() {
        for(int i = 0; i <= 10; i++){
            static_i = i;
            try {
                Thread.sleep(50*i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(static_i != i){
System.err.println("[" + Thread.currentThread().getId()   + "]當i=" + i + "獲取static_i 的值:" + static_i);
            } else {
System.out.println("[" + Thread.currentThread().getId() + "]當i=" + i + "獲取static_i 的值:" + static_i);
            }
        }
    }
}

4.1.1.2.StaticMain.java代碼
public class StaticMain{
    public static void main(String[] args) {
        // 啓動儘量多的線程才能很容易的模擬出問題
        for (int i = 0; i < 10; i++) {
            StaticVariableTest t = new StaticVariableTest();
            t.start();
        }
    }
}

4.1.1.3.執行結果
 
4.1.1.4.分析結果

出現圖中的結果場景,當線程15執行了static_i = 9; 後,線程15進入了sleep狀態(在業務系統中可能是執行很多其他的業務代碼),這時候當某個線程剛好執行到static_i = 10;static_i醒來這時候static_i的值已經被改爲10了,線程15繼續執行下面的判斷if(static_i != i)就出現了上面的結果。

4.2.    實例變量

實例變量爲對象實例私有,在JVM內存的堆中分配,若在系統中只存在一個此對象的實例,在多線程環境下,“猶如”靜態變量那樣,被某個線程修改後,其他線程對修改均可見,故線程非安全;如果每個線程執行都是在不同的對象中,那對象與對象之間的實例變量的修改將互不影響,故線程安全。

4.2.1.實例變量線程不安全測試代碼

4.2.1.1.InstanceVariableTest.java代碼
public class InstanceVariableTest {
    /**
     * 實例變量(當InstanceVariableTest爲單例的時候是共享資源)
     */
    private int instance_i;

private static InstanceVariableTest instanceVariableTest = new InstanceVariableTest();
    
    /**
     * 把構造器私有化,確保單例
     */
    private InstanceVariableTest(){}
    
    public static InstanceVariableTest getInstance(){
        return instanceVariableTest;
    }
    
    public void runInstanceVariableTest() {
        for(int i = 0; i <= 10; i++){
            instance_i = i;
            try {
                Thread.sleep(50*i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(instance_i != i){
System.err.println("[" + Thread.currentThread().getId()   + "]當i=" + i + "獲取static_i 的值:" + instance_i);
            } else {
System.out.println("[" + Thread.currentThread().getId() + "]當i=" + i + "獲取static_i 的值:" + instance_i);
            }
        }
    }
}

4.2.1.2.InstanceThread.java代碼
public class InstanceThread extends Thread {
    public void run() {
        InstanceVariableLockTest ivt = InstanceVariableLockTest.getInstance();
        ivt.runInstanceVariableTest();
    }
}

4.2.1.3.InstanceMain.java代碼
public class InstanceMain {
    
    public static void main(String[] args) {
        // 啓動儘量多的線程才能很容易的模擬出問題
        for (int i = 0; i < 10; i++) {
            InstanceLockThread t = new InstanceLockThread();
            t.start();
        }
    }
}

4.2.1.4.執行結果

4.2.1.5.結果分析

出現圖中的結果場景,當線程11執行了static_i = 5; 後,線程11進入了sleep狀態(在業務系統中可能是執行很多其他的業務代碼),這時候當某個線程剛好執行到static_i = 6;static_i醒來這時候static_i的值已經被改爲6了,線程11繼續執行下面的判斷if(static_i != i)就出現了上面的結果。

4.3.局部變量

每個線程執行時將會把局部變量放在各自棧幀的工作內存中,線程間不共享,故不存在線程安全問題。

4.3.1.局部變量線程安全測試代碼

注:這裏所說的局部變量是指在方法內部聲明和定義的變量,不包括方法傳遞的參數(從某種角度上說參數也算是局部變量),當方法的參數傳遞的是一個對象引用的時候也會存在線程安全問題。
具體的測試代碼見4.4

4.3.1.1.LocalVariableTest.java代碼
public class LocalVariableTest extends Thread {
    public void run() {
        
        /**
         * 局部變量(非共享資源)
         */
        int local_i;
        
        for(int i = 0; i <= 10; i++){
            local_i = i;
            try {
                Thread.sleep(50*i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(local_i != i){
System.err.println("[" + Thread.currentThread().getId()   + "]當i=" + i + "獲取static_i 的值:" + local_i);
            } else {
System.out.println("[" + Thread.currentThread().getId() + "]當i=" + i + "獲取static_i 的值:" + local_i);
            }
        }
    }
}

4.3.1.2.LocalMain.java代碼
public class LocalMain {
    
    public static void main(String[] args) {
        // 啓動儘量多的線程才能很容易的模擬出問題
        for (int i = 0; i < 10; i++) {
            LocalVariableTest t = new LocalVariableTest();
            t.start();
        }
    }
}

4.3.1.3.執行結果

4.3.1.4.結果分析

由於每個線程執行時將會把局部變量放在各自棧幀的工作內存中,線程間是不共享的,這樣就不滿足造成線程不安全的唯一條件“存在多個線程同時修改一個對象實例”,所以局部變量是線程安全的。

4.4.引用參數傳遞的線程不安全測試代碼
 

4.4.1.User.java代碼
public class User {
    
    private String name;

    private static User user = new User("zhangsan");;
    
    private User(){}
    private User(String name){}
    
    public static User getUser(){
        return user;
    }
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

4.4.2.ReferParamTransTest.java代碼
public class ReferParamTransTest {
    
    public void referParamTransRun(User user){
        for(int i = 0; i <= 10; i++){
            String newName = "name" + Thread.currentThread().getId();
            user.setName(newName);
            try {
                Thread.sleep(50*i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(newName.equals(user.getName())){
System.out.println("[" + Thread.currentThread().getId() + "]當name=" + newName + "獲取name 的值:" + user.getName());
            } else {
System.err.println("[" + Thread.currentThread().getId() + "]當name=" + newName + "獲取name 的值:" + user.getName());
            }
        }
    }
}

4.4.3.ReferParamTransThread.java代碼
public class ReferParamTransThread extends Thread {

    public void run() {
        ReferParamTransTest rt = new ReferParamTransTest();
        rt.referParamTransRun(User.getUser());
    }
}

4.4.4.ReferParamTransMain.java代碼
public class ReferParamTransMain {

    public static void main(String[] args) {
        for(int i=0; i<10; i++){
            ReferParamTransThread t = new ReferParamTransThread();
            t.start();
        }
    }
}

4.4.5.執行結果

4.4.6.結果分析

這個例子中的user對象雖然從頭到尾都是被當作參數在方法中傳遞,但是由於User對象是單例的,所以多線程的情況下不同線程中方法中傳遞的user對象都是同一個實例對象(共享資源)。同時,由於java中的非基本數據類型(注1)對象參數傳遞採用的是“引用傳遞”,而不是 “值傳遞”,所以一個線程在接受到user實例對象的referParamTransRun()方法中隊user實例對象做了修改,對其它的線程是可見的。這樣就會出現下面的場景:當線程11給user對象的name屬性賦值爲name12之後,進入睡眠狀態,這個時候線程12將ser實例對象的name屬性修改爲name12,當name11醒來的時候繼續執行下面的代碼用到的user對象就不是自己想象的了,而是被線程12篡改過的。這樣就導致了線程不安全問題。

注1:java中非基本數據類型中的String是個例外,它和基本數據類型一樣也是採用的是值傳遞。

5.如何避免線程不安全

  • 儘量使用局部變量:這樣做是爲了不滿足造成線程不安全條件中的“存在共享資源”。
  • 儘量不去做對共享資源修改的操作:這樣做是通過不對共享資源進行修改,從而達到不滿足造成線程不安全條件中的“不同線程同時對共享資源修改”的目的。
  • 對共享資源加鎖:若必須用到共享資源又需要對其修改資源,那就對共享資源加鎖,確保在一個時刻只有一個線程在操作它。最常用的加鎖方式就是添加synchronized關鍵字
  • 使用線程安全的類:若必須用到共享資源又需要對其修改,那就使用線程安全的類,如:Map要用java.util.concurrent包下面的ConcurrentHashMap而不能用 HashMap 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章