熟悉java或如C#等使用共享內存模型作為并發實現的人都比較清楚,編寫線程安全的代碼很關鍵的一點就是要控制好可變狀態,對于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次異常);
拿剛剛上面有可變狀態的代碼來說,如果那段代碼是在多線程中執行那么就會可能出現異常結果:
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
新聞熱點
疑難解答