【軟件構造】第十章 線程和分佈式系統

第十章 線程和分佈式系統

本章關注複雜軟件系統的構造。 本章關注複雜軟件系統的構造。 這裏的「複雜」包括三方面: 這裏的「複雜」包括三方面: (1)多線程序 (2)分佈式程序 (3) GUI 程序html

Outline

  • 併發編程
    • Shared memory
    • Message passing
  • 進程和線程
  • 線程的建立和啓動,runable
  • 時間分片、交錯執行、競爭條件
  • 線程的休眠、中斷
  • 線程安全的四種策略
    • 約束(Confinement)
    • 不變性
    • 使用線程安全的數據類型
    • 同步與鎖
  • 死鎖
  • 以註釋的形式撰寫線程安全策略

Notes

## 併發編程

【併發(concurrency)】java

  • 定義:指的是多線程場景下對共享資源的爭奪運行
  • 併發的應用背景:
    • 網絡上的多臺計算機
    • 一臺計算機上的多個應用
    • 一個CPU上的多核處理器
  • 爲何要有併發:
    • 摩爾定律失效、「核」變得愈來愈多
    • 爲了充分利用多核和多處理器須要將程序轉化爲並行執行
  • 併發編程的兩種模式:
    • 共享內存:在內存中讀寫共享數據
    • 信息傳遞(Message Passing):經過channel交換消息

【共享內存】android

  • 共享內存這種方式比較常見,咱們常常會設置一個共享變量,而後多個線程去操做同一個共享變量。從而達到線程通信的目的。
  • 例子:
    • 兩個處理器,共享內存
    • 同一臺機器上的兩個程序,共享文件系統
    • 同一個Java程序內的兩個線程,共享Java對象

【信息傳遞】程序員

  • 消息傳遞方式採起的是線程之間的直接通訊,不一樣的線程之間經過顯式的發送消息來達到交互目的
  • 接收方將收到的消息造成隊列逐一處理,消息發送者繼續發送(異步方式)
  • 消息傳遞機制也沒法解決競爭條件問題
  • 仍然存在消息傳遞時間上的交錯
  • 例子:
    • 網絡上的兩臺計算機,經過網絡鏈接通信
    • 瀏覽器和Web服務器,A請求頁面,B發送頁面數據給A
    • 即時通信軟件的客戶端和服務器
    • 同一臺計算機上的兩個程序,經過管道鏈接進行通信

 

 

併發模型 通訊機制 同步機制
共享內存

線程之間共享程序的公共狀態,線程之間經過寫-讀內存中的公共狀態來隱式進行通訊。編程

同步是顯式進行的。程序員必須顯式指定某個方法或某段代碼須要在線程之間互斥執行。api

消息傳遞

線程之間沒有公共狀態,線程之間必須經過明確的發送消息來顯式進行通訊。瀏覽器

因爲消息的發送必須在消息的接收以前,所以同步是隱式進行的。安全

 

## 進程和線程

  • 進程:是執行中一段程序,即一旦程序被載入到內存中並準備執行,它就是一個進程。進程是表示資源分配的的基本概念,又是調度運行的基本單位,是系統中的併發執行的單位。
    • 程序運行時在內存中分配本身獨立的運行空間
    • 進程擁有整臺計算機的資源
    • 多進程之間不共享內存
    • 進程之間經過消息傳遞進行協做
    • 通常來講,進程==程序==應用(但一個應用中可能包含多個進程)
    • OS支持的IPC機制(pipe/socket)支持進程間通訊(IPC不只是本機的多個進程之間, 也能夠是不一樣機器的多個進程之間)
    • JVM一般運行單一進程,但也能夠建立新的進程。
  • 線程:它是位於進程中,負責當前進程中的某個具有獨立運行資格的空間。
    • 線程有本身的堆棧和局部變量,可是多個線程共享內存空間
    • 進程=虛擬機;線程=虛擬CPU
    • 程序共享、資源共享,都隸屬於進程
    • 很難得到線程私有的內存空間
    • 線程須要同步:在改變對象時要保持lock狀態
    • 清理線程是不安全的
  • 進程是負責整個程序的運行,而線程是程序中具體的某個獨立功能的運行。
  • 一個進程中至少應該有一個線程。
  • 主線程能夠建立其餘的線程。

## 線程的建立和啓動,runable

【方式1:繼承Thread類】服務器

  • 方法:用Thread類實現了Runnable接口,但它其中的run方法什麼都沒作,因此用一個類作Thread的子類,提供它本身實現的run方法。用Thread.start()來開始一個新的線程。
  • 建立:A類   a  =  new   A類();
  • 啓動: a.start();
  • 步驟:
    • 定義一個類A繼承於java.lang.Thread類.
    • 在A類中覆蓋Thread類中的run方法.
    • 咱們在run方法中編寫須要執行的操做:run方法裏的代碼,線程執行體.
    • 在main方法(線程)中,建立線程對象,並啓動線程.
  • 栗子:
 1 //1):定義一個類A繼承於java.lang.Thread類.  
 2 class MusicThread extends Thread{  
 3     //2):在A類中覆蓋Thread類中的run方法.  
 4     public void run() {  
 5         //3):在run方法中編寫須要執行的操做  
 6         for(int i = 0; i < 50; i ++){  
 7             System.out.println("播放音樂"+i);  
 8         }  
 9     }  
10 }  
11   
12 public class ExtendsThreadDemo {  
13     public static void main(String[] args) {  
14           
15         for(int j = 0; j < 50; j ++){  
16             System.out.println("運行遊戲"+j);  
17             if(j == 10){  
18                 //4):在main方法(線程)中,建立線程對象,並啓動線程.  
19                 MusicThread music = new MusicThread();  
20                 music.start();  
21             }  
22         }  
23     }  
25 }  

 

方式2:實現Runable接口網絡

  • 建立:Thread  t = new Thread(new  A());
  • 調用:t.start();
  • 步驟:
    • 定義一個類A實現於java.lang.Runnable接口,注意A類不是線程類.
    • 在A類中覆蓋Runnable接口中的run方法.
    • 咱們在run方法中編寫須要執行的操做:run方法裏的,線程執行體.
    • 在main方法(線程)中,建立線程對象,並啓動線程.
 1 //1):定義一個類A實現於java.lang.Runnable接口,注意A類不是線程類.  
 2 class MusicImplements implements Runnable{  
 3     //2):在A類中覆蓋Runnable接口中的run方法.  
 4     public void run() {  
 5         //3):在run方法中編寫須要執行的操做  
 6         for(int i = 0; i < 50; i ++){  
 7             System.out.println("播放音樂"+i);  
 8         }  
 9           
10     }  
11 }  
12   
13 public class ImplementsRunnableDemo {  
14     public static void main(String[] args) {  
15         for(int j = 0; j < 50; j ++){  
16             System.out.println("運行遊戲"+j);  
17             if(j == 10){  
18                 //4):在main方法(線程)中,建立線程對象,並啓動線程  
19                 MusicImplements mi = new MusicImplements();  
20                 Thread t = new Thread(mi);  
21                 t.start();  
22             }  
23         }  
24     }     

 

  • 實現Runnable接口相比繼承Thread類有以下好處:
    • 避免點繼承的侷限,一個類能夠繼承多個接口。
    • 適合於資源的共享
  • 建立並運行一個線程所犯的常見錯誤是調用線程的 run()方法而非 start()方法,以下所示:
Thread newThread = new Thread(MyRunnable());
newThread.run();  //should be start();

  起初並不會感受到有什麼不妥,由於 run()方法的確如你所願的被調用了。可是,事實上,run()方法並不是是由剛建立的新線程所執行的,而是被建立新線程的當前線程所執行了。也就是被執行上面兩行代碼的線程所執行的。想要讓建立的新線程執行 run()方法,必須調用新線程的 start 方法。

 

## 時間分片、交錯執行、競爭條件

 【時間分片】

  • 雖然有多線程,但只有一個核,每一個時刻只能執行一個線程。
    • 經過時間分片,再多個線程/進程之間共享處理器
  • 即便是多核CPU,進程/線程的數目也每每大於核的數目
  • 經過時間分片,在多個進程/線程之間共享處理器。(時間分片是由OS自動調度的)
  • 當線程數多於處理器數量時,併發性經過時間片來模擬,處理器切換處理不一樣的線程

 【交錯執行】

  顧名思義,就是說在線程運行的過程當中,多個線程同時運行相互交錯。並且,因爲線程運行通常不是連續的,那麼就會致使線程間的交錯。能夠說,全部線程安全問題的本質都是線程交錯的問題。

 【競爭條件】

  競爭是發生在線程交錯的基礎上的。當多個線程對同一對象進行讀寫訪問時,就可能會致使競爭的問題。程序中可能出現的一種問題就是,讀寫數據發生了不一樣步。例如,我要用一個數據,在該數據修改還沒寫回內存中時就讀取出來了,那麼就會致使程序出現問題。

  程序運行時有一種狀況,就是程序若是要正確運行,必須保證A線程在B線程以前完成(正確性意味着程序運行知足其規約)。當發生這種狀況時,就能夠說A與B發生競爭關係。

  • 計算機運行過程當中,併發、無序、大量的進程在使用有限、獨佔、不可搶佔的資源,因爲進程無限,資源有限,產生矛盾,這種矛盾稱爲競爭(Race)。
  • 因爲兩個或者多個進程競爭使用不能被同時訪問的資源,使得這些進程有可能由於時間上推動的前後緣由而出現問題,這叫作競爭條件(Race Condition)。
  • 競爭條件分爲兩類: 
    -Mutex(互斥):兩個或多個進程彼此之間沒有內在的制約關係,可是因爲要搶佔使用某個臨界資源(不能被多個進程同時使用的資源,如打印機,變量)而產生制約關係。 
    -Synchronization(同步):兩個或多個進程彼此之間存在內在的制約關係(前一個進程執行完,其餘的進程才能執行),如嚴格輪轉法。
  • 解決互斥方法: 
    Busy Waiting(忙等待):等着可是不停的檢查測試,不睡覺,知道能進行爲止 
    Sleep and Wakeup(睡眠與喚醒):引入Semapgore(信號量,包含整數和等待隊列,爲進程睡覺而設置),喚醒由其餘進程引起。
  • 臨界區(Critical Region):
    • 一段訪問臨界資源的代碼。
    • 爲了不出現競爭條件,進入臨界區要遵循四條原則: 
      • 任何兩個進程不能同時進入訪問同一臨界資源的臨界區
      • 進程的個數,CPU個數性能等都是無序的,隨機的
      • 臨界區以外的進程不得阻塞其餘進程進入臨界區
      • 任何進程都不該被長期阻塞在臨界區以外
  • 解決互斥的方法: 
    • 禁用中斷 Disabling interrupts 
    • 鎖變量 Lock variables (no) 
    • 嚴格輪轉 Strict alternation (no) 
    • Peterson’s solution (yes) 
    • The TSL instruction (yes) 

 

 ##  線程的休眠、中斷

 【Thread.sleep】

  • 在線程中容許一個線程進行暫時的休眠,直接使用Thread.sleep()方法便可。 
    • 將某個線程休眠,意味着其餘線程獲得更多的執行機會
    • 進入休眠的線程不會失去對現有monitor或鎖的全部權
  • sleep定義格式:
public static void sleep(long milis,int nanos)
       throws InterruptedException

  首先,static,說明能夠由Thread類名稱調用,其次throws表示若是有異常要在調用此方法到處理異常

因此sleep()方法要有InterruptedException 異常處理,並且sleep()調用方法一般爲Thread.sleep(500) ;形式。

  • 實例:

 

【Thread.interrupt】 

  • 一個線程能夠被另外一個線程中斷其操做的狀態,使用 interrupt()方法完成。
    • 經過線程的實例來調用interrupt()函數,向線程發出中斷信號
    •  t.interrupt():在其餘線程裏向t發出中斷信號
    •  t.isInterrupted():檢查t是否已在中斷狀態中
  • 當某個線程被中斷後,通常來講應中止 其run()中的執行,取決於程序員在run()中處理
    • 通常來講,線 程在收到中斷信號時應該中斷,直接終止
    • 可是,線程收到其餘線程發出來的中斷信號,並不意味着必定要「中止」
  •  實例:

 

  • 實例二:
package Thread1;
class MyThread implements Runnable{    // 實現Runnable接口
    public void run(){    // 覆寫run()方法
        System.out.println("一、進入run()方法") ;
        try{
                Thread.sleep(10000) ;    // 線程休眠10秒
                System.out.println("二、已經完成了休眠") ;
        }catch(InterruptedException e){
            System.out.println("三、休眠被終止") ;
            return ; // 返回調用處
        }
        System.out.println("四、run()方法正常結束") ;
    }
};
public class demo1{
    public static void main(String args[]){
        MyThread mt = new MyThread() ;    // 實例化Runnable子類對象
        Thread t = new Thread(mt,"線程");        // 實例化Thread對象
        t.start() ;    // 啓動線程
        try{
                Thread.sleep(2000) ;    // 線程休眠2秒
        }catch(InterruptedException e){
            System.out.println("三、休眠被終止") ;
        }
        t.interrupt() ;    // 中斷線程執行
    }
};

運行結果:

一、進入run()方法
三、休眠被終止

 

 ## 線程安全的四個策略

  • 線程安全的定義:ADT或方法在多線程中要執行正確,即不管如何執行,不準調度者作額外的協做,都能知足正確性
  • 四種線程安全的策略:
    • Confinement 限制數據共享
    • Immutability 共享不可變數據
    • Threadsafe data type 共享線程安全的可 變數據
    • Synchronization 同步機制共享共享線程 不安全的可變數據,對外即爲線程安全的ADT.

【Confinement 限制數據共享】

  • 核心思想:線程之間不共享mutable數據類型
    • 將可變數據限制在單一線程內部,避免競爭
    • 不容許任何縣城直接讀寫該數據
  • 在多線程環境中,取消全局變量,儘可能避免使用不安全的靜態變量。
    • 限制數據共享主要是在線程內部使用局部變量,由於局部變量在每一個函數的棧內,每一個函數都有本身的棧結構,互不影響,這樣局部變量之間也互不影響。
    • 若是局部變量是一個指向對象的引用,那麼就須要檢查該對象是否被限制住,若是沒有被限制住(便可以被其餘線程所訪問),那麼就沒有限制住數據,所以也就不能用這種方法來保證線程安全
  •  栗子: 
public class Factorial {

    /**
     * Computes n! and prints it on standard output.
     * @param n must be >= 0
     */
    private static void computeFact(final int n) {
        BigInteger result = new BigInteger("1");
        for (int i = 1; i <= n; ++i) {
            System.out.println("working on fact " + n);
            result = result.multiply(new BigInteger(String.valueOf(i)));
        }
        System.out.println("fact(" + n + ") = " + result);
    }

    public static void main(String[] args) {
        new Thread(new Runnable() { // create a thread using an
            public void run() {     // anonymous Runnable
                computeFact(99);
            }
        }).start();
        computeFact(100);
    }
}

 

 解釋:主函數開啓了兩個線程,調用的是相同函數。由於線程共享局部變量的類型,但每一個函數調用有不一樣的棧,所以有不一樣的i,n,result。因爲每一個函數都有本身的局部變量,那麼每一個函數就能夠獨立運行,更新它們本身的函數值,線程之間不影響結果。

 

【Immutability 共享不可變數據】 

不可變數據類型,指那些在整個程序運行過程當中,指向內存的引用是一直不變的,一般使用final來修飾。不可變數據類型一般來說是線程安全的,但也可能發生意外。

可是,程序在運行過程當中,有時爲了優化程序結構,默默地將這個引用更改了。此時,客戶端程序員是不知道它被更改了,對於客戶端而言,這個引用仍是不可變的,但其實已經被悄悄更改了。這時就會發生一些線程安全問題。

解決方案就是給這些不可變數據類型再增長一些限制:

  • 全部的方法和屬性都是私有的。
  • 不提供可變的方法,即不對外開放能夠更改內部屬性的方法。
  • 沒有數據的泄露,即返回值而不是引用。
  • 不在其中存儲可變數據對象。

這樣就能夠保證線程的安全了。

 

【Threadsafe data type(共享線程安全的可變數據)】

  • 方法:若是必需要用mutable的數據類型在多線程之間共享數據,要使用線程安全的數據類型。(在JDK中的類,文檔中明確指明瞭是否threadsafe)
  • 通常來講,JDK同時提供兩個相同功能的類,一個是threadsafe,另外一個不是。緣由:threadsafe的類通常性能上受影響。
  • List、Set、Map這些集合類都是線程不安全的,Java API爲這些集合類提供了進一步的decorator
 private static Map<Integer,Boolean> cache = Collections.synchronizedMap(new HashMap<>());
 public static <T> Collection<T> synchronizedCollection(Collection<T> c);
 public static <T> Set<T> synchronizedSet(Set<T> s);
 public static <T> List<T> synchronizedList(List<T> list);
 public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
 public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);
 public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);
  • ***在使用synchronizedMap(hashMap)以後,不要再把參數hashMap共享給其餘線程,不要保留別名,必定要完全銷燬.(能夠用private static Map cache =Collections.synchronizedMap(new HashMap<>());的方式實例化集合類)
  • 即便在線程安全的集合類上,使用iterator也 是不安全的:
List<Type> c = Collections.synchronizedList(new
ArrayList<Type>());
synchronized(c) { // to be introduced later (the 4-th threadsafe way)
    for (Type e : c)
        foo(e);
}
  • 須要注意用java提供的包裝類包裝集合後,只是將集合的每一個操做都當作了原子操做,也就保證了每一個操做內部的正確性,可是在兩個操做之間不能保證集合類不被修改,所以須要用lock機制,例如

  若是在isEmpty和get中間,將元素移除,也就產生了競爭。

 

前三種策略的核心思想:避免共享 --> 即便共享,也只能讀/不可寫(immutable) -->即便可寫(mutable),共享的可寫數據應本身具有在多線程之間協調的能力,即「使用線程安全的mutable ADT」

 【Synchronization 同步與鎖】

  • 爲何要同步
    • java容許多線程併發控制,當多個線程同時操做一個可共享的資源變量時(如數據的增刪改查)
    • 將會致使數據不許確,相互之間產生衝突,所以加入同步鎖以免在該線程沒有完成操做以前,被其餘線程的調用,
    • 從而保證了該變量的惟一性和準確性。
  • 同步方法
    • 即有synchronized關鍵字修飾的方法。
    • 因爲java的每一個對象都有一個內置鎖,當用此關鍵字修飾方法時,內置鎖會保護整個方法。
    • 在調用該方法前,須要得到內置鎖,不然就處於阻塞狀態。
    • 代碼以下:
      public synchronized void save(){} 
    •   注: synchronized關鍵字也能夠修飾靜態方法,此時若是調用該靜態方法,將會鎖住整個類
  •  同步代碼塊
    • 在調用該方法前,須要得到內置鎖,不然就處於阻塞狀態。
    • 被該關鍵字修飾的語句塊會自動被加上內置鎖,從而實現同步。
    • 代碼如:
      synchronized(object){    }
    • 注:同步是一種高開銷的操做,所以應該儘可能減小同步的內容。
  • 使用鎖機制,得到對數據的獨家mutation權,其餘線程被阻塞,不得訪問
  • Lock是Java語言提供的內嵌機制,每一個object都有相關聯的lock
  • 任何共享的mutable變量/對象必須被lock所保護
  • 涉及到多個mutable變量的時候,它們必須被同一個lock所保護

 

 ## 死鎖

  • 定義:兩個或多個線程相互等待對方釋放鎖,則會出現死鎖現象。
  • java虛擬機沒有檢測,也沒有采用措施來處理死鎖狀況,因此多線程編程是應該採起措施避免死鎖的出現。一旦出現死鎖,整個程序即不會發生任何異常,也不會給出任何提示,只是全部線程都處於堵塞狀態。
  • 造成死鎖的條件:
    • 互斥條件:線程使用的資源必須至少有一個是不能共享的(至少有鎖);
    • 請求與保持條件:至少有一個線程必須持有一個資源而且正在等待獲取一個當前被其它線程持有的資源(至少兩個線程持有不一樣鎖,又在等待對方持有鎖);
    • 非剝奪條件:分配資源不能從相應的線程中被強制剝奪(不能強行獲取被其餘線程持有鎖);
    • 循環等待條件:第一個線程等待其它線程,後者又在等待第一個線程(線程A等線程B;線程B等線程C;...;線程N等線程A。如此造成環路)。
  • 防止死鎖的方法:
    • 加鎖順序:當多個線程須要相同的一些鎖,可是按照不一樣的順序加鎖,死鎖就很容易發生。若是能確保全部的線程都是按照相同的順序得到鎖,那麼死鎖就不會發生。這種方式是一種有效的死鎖預防機制。可是,這種方式須要你事先知道全部可能會用到的鎖,但總有些時候是沒法預知的

    • 使用粗粒度的鎖,用單個鎖來監控多個對象
      • 對整個社交網 絡設置 一個鎖 ,而且對其任何組成部分的全部操做都在該鎖上進行同步。
      • 例如:全部的Wizards都屬於一個Castle,  可以使用 castle 實例的鎖
        缺點:性能損失大;
      • 若是用一個鎖保護大量的可變數據,那麼久放棄了同時訪問這些數據的能力;
      • 在最糟糕的狀況下,程序可能基本上是順序執行的,喪失了併發性

    • 加鎖時限:在嘗試獲取鎖的時候加一個超時時間,這也就意味着在嘗試獲取鎖的過程當中若超過了這個時限該線程則放棄對該鎖請求。若一個線程沒有在給定的時限內成功得到全部須要的鎖,則會進行回退並釋放全部已經得到的鎖。
    • 用 jstack 等工具進行死鎖檢測

## 以註釋的形式撰寫線程安全策略

  • 在代碼中以註釋的形式添加說明:該ADT採起了什麼設計決策來保證線程安全
  • 闡述如何使rep線程安全;
  • 寫入表示不變性的說明中,以便代碼維護者知道你是如何爲類設計線程安全性的。
  • 須要對安全性進行這種仔細的論證,闡述使用了哪一種技術,使用threadsafe data types, or synchronization時,須要論證全部對數據的訪問都是具備原子性的
  • 栗子:

  • 反例

  • 字符串是不可變的而且是線程安全的; 可是指向該字符串的rep,特別是文本變量,並非不可變的;
  • 文本不是最終變量,由於咱們須要數據類型來支持插入和刪除操做;
  • 所以讀取和寫入文本變量自己不是線程安全的。
相關文章
相關標籤/搜索