第一章 線程及線程安全

1.線程介紹

線程,也稱爲輕量級進程。大多數現代操做系統把線程做爲時序調度的基本單元。多線程的出現給程序處理帶來了不少新的方式,但同時線程的使用也存在着很多風險。java

線程的優勢

  • 多處理器的出現。程序調度的基本單元是線程,一個單線程應用程序一次只能運行在一個處理器上。於是多處理器的出現使得多線程的使用可以更充分地利用處理器的資源。
  • 對異步事件的處理。程序中有一些I/O或者等待資源的操做,單線程的狀況下,整個程序都會中止來等待返回結果。而多線程的狀況下,能夠將這樣耗時的操做做爲異步的操做來同時進行。
  • 用戶界面更好的響應。好比在GUI框架中,觸發一個事件後,主線程會將事件交給獨立的事件處理器線程來操做,從而使用戶界面上不會出現卡頓或者沒有反應的現象。

線程的風險

安全危險安全

在多線程的狀況下,若是程序沒有進行充分的同步,程序的執行順序是沒法預測的。好比下面的類:多線程

public class UnsafeSequence {
    private int value;
    
    public int getNext(){
        return value++;
    }
}

在單線程的狀況下,調用getNext方法得到的結果是毋庸置疑的。可是在多線程的狀況下,這個方法就是非線程安全的。雖然只有value++一個操做,可是自增操做並非一個原子操做。getNext可否返回正確的值,取決於運行時線程交替執行的順序,也稱之爲競爭條件(race condition)。併發

活躍性危險框架

多線程的使用可能會引發不少非必現的bug。好比線程A和線程B共同操做一個資源,若是線程B佔有該資源一直不釋放,則線程A須要一直等待,從而引發活躍度失敗。然而因爲不少bug是線程間執行時間的時序問題引起的,在開發和測試的過程當中,每每難以發現。異步

性能危險ide

多線程的使用確實能併發地去執行多個任務,但多線程的建立銷燬以及線程的切換仍然會帶來必定程度的性能開銷。如線程切換時保存當前線程的上下文以及恢復下一個線程的上下文。若是線程間共享數據,須要使用同步機制,這個機制一樣會消耗額外的性能。性能

2.線程安全

什麼叫線程安全

簡單的來講,就是在多個線程訪問一個類的時候,該類始終保持着正確的執行行爲。測試

這裏包括不用考慮線程在運行時的調度和交替執行的時序,以及不須要額外的同步或協調。操作系統

無狀態對象永遠是線程安全的

若是兩個線程不共享狀態,即沒有共享數據,則運行時相互之間沒有任何的交互,也就不存在線程安全的問題。

原子性與競爭條件

原子性,顧名思義,指的就是不可分割的操做,多個操做要麼一塊兒執行,要麼就都不執行。原子性保證了程序的執行不會由於執行的時序問題而引起的線程安全問題。

而常常引發原子性問題的就是競爭條件。好比常見的檢查再運行(check-then-act),當我建立一個文件夾時,會先判斷文件夾是否存在,不存在再建立。這個在單線程的狀況下不會出現問題,可是在多線程下,就可能會由於當我檢查文件夾不存在後,另外一個線程先建立了該文件夾,從而致使此線程建立文件錯誤。

public void mkdir(){
    File dest = new File("E://temp");
    if(!dest.exists()){
        dest.mkdir();
    }
}

不少複合操做在多線程下都會出現這樣的問題,而解決這個問題的方法就是鎖。

java提供了強制原子性的內置鎖機制:synchronized塊。synchronized是一個互斥鎖,意味最多隻有一個線程能夠擁有,若是其餘線程嘗試獲取,則必須等待或者阻塞,直到擁有鎖的線程釋放鎖。回到剛纔的建立文件夾的例子,咱們就能夠經過synchronized塊解決其線程安全的問題。

public synchronized void mkdir(){
    File dest = new File("E://temp");
    if(!dest.exists()){
        dest.mkdir();
    }
}

內部鎖是可重進入的,這裏的重進入是對擁有鎖的同一個線程。當線程試圖獲取它本身佔有的鎖時,請求會成功。這就意味着鎖的請求時基於「每線程(per-thread)」,而不是基於「每調用(per-invocation)」。

重進入的實現是經過給每一個鎖關聯一個請求計數和一個佔有它的線程。當計數爲0時,認爲鎖是未被佔有的;線程請求一個未被佔有的鎖時,JVM將記錄佔有鎖的線程,而且將請求計數置爲1;若是同一線程再次請求鎖,計數遞增;線程每次退出同步塊,請求計數減一;直到計數爲0,線程釋放鎖。

重進入方便了鎖行爲的封裝,好比子類覆寫了父類synchronized類的方法,而且調用父類中的方法。

//父類
public class Father {
    public synchronized void doSomething(){
        //TODO
    }
}

//子類
public class Son extends Father{
    @Override
    public synchronized void doSomething() {
        System.out.println("son do something");
        super.doSomething();
    }
}

能夠看到當執行子類synchronized方法時,須要調用父類的synchronized方法,若是內部鎖不是可重進入的,此時便會出現死鎖。

用鎖來保護狀態

每一個共享變量都須要惟一肯定的鎖來保護其狀態。對於涉及多個變量的不變約束 ,須要同一個鎖來保護其全部的變量。如Vector類自己每一個方法都是同步的,但一旦涉及到多個方法的複合操做,仍是須要鎖對整個複合操做進行同步。

關於線程的簡單介紹就到這裏了,下一節來討論關於多線程中對象的使用。

相關文章
相關標籤/搜索