併發編程之可變狀態

熟悉Java或如C#等使用共享內存模型做爲併發實現的人都比較清楚,編寫線程安全的代碼很關鍵的一點就是要控制好可變狀態,對於Java開發者來講可能用內存可見性更容易理解,在各類關於併發的書籍中都是處理好內存可見性問題編寫線程安全的代碼就成功了一半了,但我認爲「內存可見性」太過於抽象、底層,使開發者不容易理解;
  多線程之間經過共享內存進行通信這句話可能不少人都比較清楚,我認爲也能夠這麼說多線程間經過共享可變狀態進行通信,本篇文章討論的是命令式編程併發中的可變狀態與爲何函數式編程更容易寫出併發程序;java

可變狀態與不可變狀態

從字面上理解,狀態:某一事務所處的情況;可變:能夠變化的;
那麼可變狀態能夠理解成事務的情況是能夠變化的,如從固態到液態或到氣態;編程

可變狀態
那麼在程序中可變狀態是怎樣的呢,請閱讀下面代碼:安全

public class VariableState { 
private int variableInterval=5; 
public int  increment(int x){
   variableInterval=x+variableInterval;
   return variableInterval;
} 

public static void main(String[] args) { 
    VariableState variable=new VariableState();
    variable.increment(5);          //print 10
    //variable.variableInterval=6; 
    variable.increment(5);          //print 15 去掉註釋時 print 11
 }
}

在這段代碼中函數increment的輸出結果會隨着可變狀態variableInterval的變化而變化;多線程

不可變狀態
有可變的就會有不可變的,繼續看不可變狀態在代碼中是怎樣的:併發

public class InvariableState {
private final int invariableInterval=5; 
public int increment(int x){
    x=x+invariableInterval;
    return x;
 }
public static void main(String[] args){
    InvariableState invariable=new InvariableState();
    System.out.println(invariable.increment(5));   //print 10
    System.out.println(invariable.increment(5));   //print 10
 }
}

這段代碼中了invariableInterval就是不可變的狀態,無論調多少次increment函數的輸出結果都是同樣的;雖然程序中是存在着可變和不可變狀態,可是着又有什麼關係呢?編程語言

  答案是若是你的程序只是在單線程中運行那麼可變、不可變狀態對你沒有一點影響,但請注意若是你的程序是多線程程序(併發)那麼該可變狀態程序運行必定會出現異常結果(不是每次都會出現,也許運行100纔會有5次異常);
拿剛剛上面有可變狀態的代碼來講,若是那段代碼是在多線程中執行那麼就會可能出現異常結果:ide

public static void main(String[] args) throws InterruptedException {
    VariableState variable=new VariableState();
    Thread [] runnables=new Thread[2];
    for (int i = 0; i < 2; i++) {
        final int finalI = i;
        runnables[i]=new Thread() {
            @Override
            public void run() {
                System.out.println(" i=" + finalI +"  "+variable.increment(5));
            }
        };
    }
    runnables[0].start();
    runnables[1].start();
    runnables[0].join();
    runnables[1].join();
}

輸出結果:

函數式編程

  請看上面的示例,運行這段代碼程序會輸出兩個結果,也就是說出現了異常狀況,可能你們也都知道出現問題的緣由在哪,異常時由於兩個線程同時執行了variableInterval=x+variableInterval,一個線程進來執行了x+variableInterval尚未寫回variableInterval另外一個線程就進來執行x+variableInterval了,接着兩個線程都把各自的結果寫回到variableInterval中,因此就都是10;
  既然在多線程程序存在可變狀態就可能會出現異常結果那咱們該怎麼處理呢?不急,請繼續往下看;函數

在命令式語言中

在命令式編程語言中,如Java、C#等,像Python、Golang能夠說是命令式與函數式混合型的,雖然Java、C#也都加入了Lambda表達式的支持向函數式編程靠攏,但畢竟他的主流仍是命令式編程;
下面看看在Java中是如何處理可變狀態在多線程中的異常狀況的;性能

public synchronized int increment(int x) {
    variableInterval = x + variableInterval;
    return variableInterval;
}

  仍是剛剛那個示例,只是在方法上添加了synchronized關鍵字,相信不少Java都清楚這是什麼意思,這指的是在increment函數上添加了一個對象鎖,當一個線程進入該函數時必須獲取該對象鎖才能進入,每次只能一個線程進入線程退出後就會釋放該鎖。在Java中還能夠把synchronized當代碼塊、ReentrantLock、Lock等或使用不可變狀態來解決該問題;
  你可能會以爲這麼簡單的問題還須要談論麼,其實多線程與鎖問題一點都不簡單,只是這裏的示例比較簡單這裏只是簡單對象的可變狀態,若是是個複雜的對象存在可變狀態呢,如:DataParser或本身寫的複雜對象;在Java中編寫併發程序一般都會用到鎖、原子變量、不可變變量、volatile等,可變狀態是很是常見的等你使用鎖解決後又會出現死鎖問題,等解決了死鎖還存子資源競爭又可能會出現性能問題,由於線程(Thread)、鎖(Lock)用很差都會影響性能,這時候你還會以爲簡單麼;

在函數式語言中

  那麼在函數式語言中可變狀態又是怎麼處理呢?答案是你不用處理,由於在函數式語言中沒有可變狀態,不存在可變狀態也就不會遇到可變狀態帶來的各類問題;
  這裏使用一樣是運行在JVM上的函數式語言Clojure來講明不可變狀態,在Clojure中對象是不可變的沒有可變狀態也就不存在Java中的可變狀態問題;

Java的可變狀態示例:

int total=0;
public int sum(int[] numbsers){
    for(int n: numbers){
        total +=n;
    }
  return total;
}

  在上面的代碼中total是狀態可變的,在for循環的過程當中不斷的更新狀態,接下來看Clojure中狀態不可變實現方式:

(defn sum[numbers]
  (if (empty? numbers)
    0  
    (+ (first numbers) (sum(rest numbers))) 
  )
)
運行:    
user=> (sumfn[1,2,3,4])
10

  你可能會說這只是一個遞歸的實如今java中也可以實現,沒錯這只是遞歸,但Clojure還有更簡單的實現:

(defn sum [numbers]  
(reduce + numbers))

這夠簡單了吧,拋棄的可變狀態並且代碼更短了,實現併發的時候也不存在可變狀態問題;
  這裏也不是比較說哪一種更好,在合適的地方使用合適的方法最好;命令式編程與函數式編程根本的區別在於:命令式編程代碼使用一系列改變狀態的語句組成,而函數式編程把數學函數做爲第一類對象,將計算過程抽象爲表達式求值表達式由純數學函數構成;

文章首發地址:Solinx http://www.solinx.co/archives/464

相關文章
相關標籤/搜索