鍍金池/ 教程/ Java/ 線程安全與共享資源
Slipped Conditions
阻塞隊(duì)列
無阻塞算法
嵌套管程鎖死
Java 并發(fā)性和多線程介紹
死鎖
線程安全及不可變性
并發(fā)編程模型
Java 中的讀/寫鎖
剖析同步器
競(jìng)態(tài)條件與臨界區(qū)
多線程的優(yōu)點(diǎn)
CAS
線程通信
如何創(chuàng)建并運(yùn)行 java 線程
阿姆達(dá)爾定律
避免死鎖
信號(hào)量
多線程的代價(jià)
饑餓和公平
線程池
重入鎖死
Java 中的鎖
Java 內(nèi)存模型
線程安全與共享資源
Java 同步塊

線程安全與共享資源

允許被多個(gè)線程同時(shí)執(zhí)行的代碼稱作線程安全的代碼。線程安全的代碼不包含競(jìng)態(tài)條件。當(dāng)多個(gè)線程同時(shí)更新共享資源時(shí)會(huì)引發(fā)競(jìng)態(tài)條件。因此,了解 Java 線程執(zhí)行時(shí)共享了什么資源很重要。

局部變量

局部變量存儲(chǔ)在線程自己的棧中。也就是說,局部變量永遠(yuǎn)也不會(huì)被多個(gè)線程共享。所以,基礎(chǔ)類型的局部變量是線程安全的。下面是基礎(chǔ)類型的局部變量的一個(gè)例子:

public void someMethod(){

  long threadSafeInt = 0;

  threadSafeInt++;
}

局部的對(duì)象引用

對(duì)象的局部引用和基礎(chǔ)類型的局部變量不太一樣。盡管引用本身沒有被共享,但引用所指的對(duì)象并沒有存儲(chǔ)在線程的棧內(nèi)。所有的對(duì)象都存在共享堆中。如果在某個(gè)方法中創(chuàng)建的對(duì)象不會(huì)逃逸出(譯者注:即該對(duì)象不會(huì)被其它方法獲得,也不會(huì)被非局部變量引用到)該方法,那么它就是線程安全的。實(shí)際上,哪怕將這個(gè)對(duì)象作為參數(shù)傳給其它方法,只要?jiǎng)e的線程獲取不到這個(gè)對(duì)象,那它仍是線程安全的。下面是一個(gè)線程安全的局部引用樣例:

public void someMethod(){

  LocalObject localObject = new LocalObject();

  localObject.callMethod();
  method2(localObject);
}

public void method2(LocalObject localObject){
  localObject.setValue("value");
}

樣例中 LocalObject 對(duì)象沒有被方法返回,也沒有被傳遞給 someMethod()方法外的對(duì)象。每個(gè)執(zhí)行 someMethod()的線程都會(huì)創(chuàng)建自己的 LocalObject 對(duì)象,并賦值給 localObject 引用。因此,這里的 LocalObject 是線程安全的。事實(shí)上,整個(gè) someMethod()都是線程安全的。即使將 LocalObject 作為參數(shù)傳給同一個(gè)類的其它方法或其它類的方法時(shí),它仍然是線程安全的。當(dāng)然,如果 LocalObject 通過某些方法被傳給了別的線程,那它就不再是線程安全的了。

對(duì)象成員

對(duì)象成員存儲(chǔ)在堆上。如果兩個(gè)線程同時(shí)更新同一個(gè)對(duì)象的同一個(gè)成員,那這個(gè)代碼就不是線程安全的。下面是一個(gè)樣例:

public class NotThreadSafe{
    StringBuilder builder = new StringBuilder();

    public add(String text){
        this.builder.append(text);
    }    
}

如果兩個(gè)線程同時(shí)調(diào)用同一個(gè) NotThreadSafe 實(shí)例上的 add()方法,就會(huì)有競(jìng)態(tài)條件問題。例如:

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

注意兩個(gè) MyRunnable 共享了同一個(gè) NotThreadSafe 對(duì)象。因此,當(dāng)它們調(diào)用 add()方法時(shí)會(huì)造成競(jìng)態(tài)條件。

當(dāng)然,如果這兩個(gè)線程在不同的 NotThreadSafe 實(shí)例上調(diào)用 call()方法,就不會(huì)導(dǎo)致競(jìng)態(tài)條件。下面是稍微修改后的例子:

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

現(xiàn)在兩個(gè)線程都有自己?jiǎn)为?dú)的 NotThreadSafe 對(duì)象,調(diào)用 add()方法時(shí)就會(huì)互不干擾,再也不會(huì)有競(jìng)態(tài)條件問題了。所以非線程安全的對(duì)象仍可以通過某種方式來消除競(jìng)態(tài)條件。

線程控制逃逸規(guī)則

線程控制逃逸規(guī)則可以幫助你判斷代碼中對(duì)某些資源的訪問是否是線程安全的。

如果一個(gè)資源的創(chuàng)建,使用,銷毀都在同一個(gè)線程內(nèi)完成,
且永遠(yuǎn)不會(huì)脫離該線程的控制,則該資源的使用就是線程安全的。

資源可以是對(duì)象,數(shù)組,文件,數(shù)據(jù)庫連接,套接字等等。Java 中你無需主動(dòng)銷毀對(duì)象,所以“銷毀”指不再有引用指向?qū)ο蟆?/p>

即使對(duì)象本身線程安全,但如果該對(duì)象中包含其他資源(文件,數(shù)據(jù)庫連接),整個(gè)應(yīng)用也許就不再是線程安全的了。比如 2 個(gè)線程都創(chuàng)建了各自的數(shù)據(jù)庫連接,每個(gè)連接自身是線程安全的,但它們所連接到的同一個(gè)數(shù)據(jù)庫也許不是線程安全的。比如,2 個(gè)線程執(zhí)行如下代碼:

檢查記錄 X 是否存在,如果不存在,插入 X

如果兩個(gè)線程同時(shí)執(zhí)行,而且碰巧檢查的是同一個(gè)記錄,那么兩個(gè)線程最終可能都插入了記錄:

線程 1 檢查記錄 X 是否存在。檢查結(jié)果:不存在
線程 2 檢查記錄 X 是否存在。檢查結(jié)果:不存在
線程 1 插入記錄 X
線程 2 插入記錄 X

同樣的問題也會(huì)發(fā)生在文件或其他共享資源上。因此,區(qū)分某個(gè)線程控制的對(duì)象是資源本身,還是僅僅到某個(gè)資源的引用很重要。