形成類在多線程時不安全的緣由

線程安全的類定義: 不存在競態條件(類中不存在被修改的成員變量),或存在時進行了同步控制。

多線程不安全的緣由-競態條件/臨界區

同一個程序運行在多個線程中自己不會有線程安全問題,問題在於多個線程訪問共享資源時存在,如:類成員變量(普通或靜態變量),系統共享資源(文件,數據庫)等。數據庫

同時只有多個線程同時對這些資源進行了寫的操做時纔會發生線程安全問題,不對資源的進行修改時就不會存在問題。安全

// 線程不安全的計數器
public class Counter {
    // 多個線程時存在的共享成員變量,產生競態條件
    protected long count = 0;

    public void add(long value){
        //此處包含三個操做:1.獲取count當前值 2.加上value值 3.將結果值賦給count
        this.count = this.count + value;  
    }
}
複製代碼

如:線程A和B同時執行同一個Counter實例的add()方法,咱們不知道操做系統什麼時候在線程之間切換,JVM並非將該方法看成單條指令執行的,而是按下列順序執行的:bash

1. 從內存中獲取this.count的值放到寄存器中
2. 將寄存器值加value
3. 將寄存器中的結果值寫回內存
複製代碼

但多個線程執行時,會共享同一個實例的count成員變量,被調度執行時,可能會按照下列順序執行:多線程

A: 讀取內存中this.count的值0到寄存器,被掛起
B: 讀取內存中this.count的值0到寄存器
B: 將寄存器值加value=2
B: 將寄存器結果寫回內存,此時內存中this.count值爲2,執行結束
A: 再此調度A繼續執行,將寄存器值加value=3
A: 將寄存器中結果寫回內存,覆蓋原來的結果值爲3,執行結束
複製代碼

兩個線程分別對count加2和3,兩個線程執行結束後,應該爲5,實際爲3。在兩個線程交叉執行時,讀到的初始值都爲0,分別寫回2或3,後者覆蓋前者,若是不對這樣的多線程訪問進行同步控制,就會形成這種線程不安全的結果。app

  • 競態條件&臨界區

當多個線程訪問同一個資源時,對前後順序敏感,就存在競態條件。致使競態條件發生的代碼區稱爲臨界區。函數

上例中add()方法是一個臨界區,它會產生競態條件。在臨界區中使用適當的同步就能夠避免競態條件。ui

類中能形成線程不安全的共享資源

當多個線程訪問共享資源變量時,而且進行了寫操做,會引起競態條件。同時讀不會產生競態條件。this

  • 方法中的局部基本類型變量

多線程中同時運行類的方法時(包括靜態方法和成員方法),方法中局部變量會在每一個線程的堆棧空間中存在副本,對它的修改不會影響其餘線程,因此不存在線程安全問題,而成員變量根據不一樣狀況會產生線程安全問題。spa

public void someMethod(){
  long threadSafeInt = 0;
  threadSafeInt++;
}
複製代碼
  • 方法中的局部對象引用變量

對象引用存在每一個線程的線程棧中,但new出來的對象實例在共享堆中,若是在某個方法中建立的局部對象不逃逸出該方法,則該類就是線程安全的。哪怕將該對象做爲參數傳遞給其餘方法,只要其餘線程獲取不到,就仍是線程安全的。操作系統

逃逸:即該對象不會被其餘方法得到,也不會被非局部變量引用。

public void someMethod(){
  LocalObject localObject = new LocalObject();
  localObject.callMethod();
  method2(localObject);
}

public void method2(LocalObject localObject){
  localObject.setValue("value");
}
複製代碼

樣例中LocalObject對象沒有被方法返回,也沒有被傳遞給someMethod()方法外的對象。每一個執行someMethod()的線程都會建立本身的LocalObject對象,並賦值給localObject引用。所以,這裏的LocalObject是線程安全的。事實上,整個someMethod()都是線程安全的。即便將LocalObject做爲參數傳給同一個類的其它方法或其它類的方法時,它仍然是線程安全的。固然,若是LocalObject經過某些方法被傳給了別的線程,那它就再也不是線程安全的了。

  • 對象成員變量

對象成員存儲在堆上。若是兩個線程同時更新同一個對象的同一個成員,那這個代碼就不是線程安全的。

public class NotThreadSafe{
    StringBuilder builder = new StringBuilder();
    public add(String text){
        this.builder.append(text);
    }  
}
複製代碼

若是兩個線程同時調用同一個NotThreadSafe實例上的add()方法,就會有競態條件問題。

注意兩個MyRunnable共享了同一個NotThreadSafe對象。
所以,當它們調用add()方法時會形成競態條件。

NotThreadSafe sharedInstance = new NotThreadSafe();
new Thread(new MyRunnable(sharedInstance)).start();
new Thread(new MyRunnable(sharedInstance)).start();

public class MyRunnable implements Runnable{
  NotThreadSafe instance = null;
  
  public MyRunnable(NotThreadSafe instance){
    this.instance = instance;
  }
  
  public void run(){
    this.instance.add("some text");
  }
}
複製代碼
固然,若是這兩個線程在不一樣的NotThreadSafe實例上調用call()方法,
就不會致使競態條件。下面是稍微修改後的例子:

new Thread(new MyRunnable(new NotThreadSafe())).start();
new Thread(new MyRunnable(new NotThreadSafe())).start();
複製代碼

如今兩個線程都有本身單獨的NotThreadSafe對象,調用add()方法時就會互不干擾,不再會有競態條件問題了。因此非線程安全的對象仍能夠經過某種方式來消除競態條件。

  • 線程控制逃逸規則

線程安全的類:不包含競態條件,即多線程時不存在共享資源變量或存在共享資源變量時進行了適當的同步控制

不可變對象保證線程安全

咱們能夠經過建立不可變的共享對象來保證對象在線程間共享時不會被修改,從而實現線程安全。以下示例:

請注意add()方法以加法操做的結果做爲一個新的ImmutableValue類實例返回
而不是直接對它本身的value變量進行操做。

public class ImmutableValue{
    private int value = 0;
    
    public ImmutableValue(int value){
        this.value = value;
    }
    public int getValue(){
        return this.value;
    }
    
    public ImmutableValue add(int valueToAdd){
        return new ImmutableValue(this.value + valueToAdd);
    }
}
複製代碼

請注意ImmutableValue類的成員變量value是經過構造函數賦值的,而且在類中沒有set方法。這意味着一旦ImmutableValue實例被建立,value變量就不能再被修改,這就是不可變性。但你能夠經過getValue()方法讀取這個變量的值。

  • 即便一個對象是線程安全的不可變對象,但在另外一個包含這個對象的一個引用的類中,該類可能不是線程安全的。
相關文章
相關標籤/搜索