Java Concurrency in Practice——讀書筆記

Thread Safety線程安全

    線程安全編碼的核心,就是管理對狀態(state)的訪問,尤爲是對(共享shared、可變mutable)狀態的訪問。java

  • shared:指能夠被多個線程訪問的變量
  • mutable:指在其生命週期內,它的值可被改變

    一般,一個對象Object的狀態state就是他的數據data,存儲於狀態變量(state variables)如實例對象或者靜態變量,以及他所依賴的其餘對象。安全

    Java中最經常使用的同步機制是使用Synchronized關鍵字,其餘還有volatile變量, explicit locks(顯式鎖), 和atomic variables(原子變量)。網絡

概念

  1. state:狀態,怎麼理解好呢,就是(在某一給定時刻,它所存儲的信息,這裏理解爲數據data)
  2. invariant:不變性,就是用來限制state的constrains the state stored in the object.例如:
  1 public class Date {
  2     int /*@spec_public@*/ day;
  3     int /*@spec_public@*/ hour;
  4 
  5     /*@invariant 1 <= day && day <= 31; @*/ //class invariant
  6     /*@invariant 0 <= hour && hour < 24; @*/ //class invariant
  7 
  8     /*@
 9     @requires 1 <= d && d <= 31;
 10     @requires 0 <= h && h < 24;
 11     @*/
 12     public Date(int d, int h) { // constructor
 13         day = d;
 14         hour = h;
 15     }
 16 
 17     /*@
 18     @requires 1 <= d && d <= 31;
 19     @ensures day == d;
 20     @*/
 21     public void setDay(int d) {
 22         day = d;
 23     }
 24 
 25     /*@
 26     @requires 0 <= h && h < 24;
 27     @ensures hour == h;
 28     @*/
 29     public void setHour(int h) {
 30         hour = h;
 31     }
 32 }

如何作到線程安全?

  1. 不在線程間共享狀態變量(state variable)—無狀態的對象老是線程安全的。
  2. 在線程間共享不可變的狀態變量(immutable state variable)
  3. 在訪問狀態變量時,使用同步機制

什麼是線程安全?

線程安全的核心概念是:正確性。一個類是否正確,取決於它是否遵照他的規範(specification),一個好的規範,定義了以下兩點內容:多線程

  1. invariants不變性,或者叫約束條件,約束了他的狀態state
  2. postconditions後置條件,描述了操做後的影響

atomic原子性

一個無狀態的Servlet必然是線程安全的,以下:less

  1 @ThreadSafe
  2 public class StatelessFactorizer implements Servlet {
  3 	public void service(ServletRequest req, ServletResponse resp) {
  4 		BigInteger i = extractFromRequest(req);
  5 		BigInteger[] factors = factor(i);
  6 		encodeIntoResponse(resp, factors);
  7 	}
  8 }

加入一個狀態後,就再也不線程安全了。jvm

  1 @NotThreadSafe
  2 public class UnsafeCountingFactorizer implements Servlet {
  3 	private long count = 0;
  4 
  5 	public long getCount() {
  6 		return count;
  7 	}
  8 
  9 	public void service(ServletRequest req, ServletResponse resp) {
 10 		BigInteger i = extractFromRequest(req);
 11 		BigInteger[] factors = factor(i);
 12 		++count;// 非原子操做
 13 		encodeIntoResponse(resp, factors);
 14 	}
 15 }

++ 操做符並不是原子操做,它包含三步:讀值,加一,寫入(read-modify-write)ide

image

Race condition競態條件

多線程中,有可能出現因爲不恰當的執行時序而形成不正確結果的狀況,稱爲競態條件。函數

競態條件一:read-modify-write(先讀取再修改寫入)post

       最後的結果依賴於它以前的狀態值,如上++操做性能

競態條件二:check-then-act(先檢查後執行)

       示例:lazy initialization

  1 @NotThreadSafe
  2 public class LazyInitRace {
  3 	private ExpensiveObject instance = null;
  4 
  5 	public ExpensiveObject getInstance() {
  6 		if (instance == null)// check then act
  7 			instance = new ExpensiveObject();
  8 		return instance;
  9 	}
 10 }

Compound actions複合操做

避免競態條件的問題,就須要以「原子」方式執行上述操做,稱之爲「複合操做」。

解決read-modify-write這一類競態條件問題時,一般使用已有的線程安全對象來管理類的狀態,以下:

  1 @ThreadSafe
  2 public class CountingFactorizer implements Servlet {
  3 	private final AtomicLong count = new AtomicLong(0);
  4 	//使用線程安全類AtomicLong來管理count這個狀態
  5 
  6 	public long getCount() {
  7 		return count.get();
  8 	}
  9 
 10 	public void service(ServletRequest req, ServletResponse resp) {
 11 		BigInteger i = extractFromRequest(req);
 12 		BigInteger[] factors = factor(i);
 13 		count.incrementAndGet();
 14 		encodeIntoResponse(resp, factors);
 15 	}
 16 }

但這種方式沒法知足check-then-act這一類競態條件問題,以下:

  1 @NotThreadSafe
  2 public class UnsafeCachingFactorizer implements Servlet {
  3 	private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>();
  4 	private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>();
  5 
  6 	public void service(ServletRequest req, ServletResponse resp) {
  7 		BigInteger i = extractFromRequest(req);
  8 		if (i.equals(lastNumber.get()))
  9 			encodeIntoResponse(resp, lastFactors.get());
 10 		else {
 11 			BigInteger[] factors = factor(i);
 12 			lastNumber.set(i);
 13 			lastFactors.set(factors);
 14 			encodeIntoResponse(resp, factors);
 15 		}
 16 	}
 17 }

鎖Locking能夠更完美的解決複合操做的原子性問題。固然鎖也能夠解決變量的可見性問題。

Intrinsic locks內置鎖

也稱爲monitor locks監視器鎖,每個Java對象均可以被當成一個鎖,自動完成鎖的獲取和釋放,使用方式以下:

  1 synchronized (lock) {
  2  // Access or modify shared state guarded by lock
  3 }
內置鎖是一種「互斥排它鎖」,所以最多隻有一個線程能夠擁有這個鎖。

同時,內置鎖也是可重入的(Reentrancy),每一個鎖含有兩個狀態,一是獲取計數器(acquisition count),一個是全部者線程(owning thread),當count=0,鎖是可獲取狀態,當一個thread t1 獲取了一個count=0的鎖時,jvm設置這個鎖的count=1,owning thread=t1,當t1再次要獲取這個鎖時,是被容許的(便可重入),此時count++,當t1退出該同步代碼塊時,count--,直到count=0後,即鎖被t1完全釋放。

如何使用lock來保護state?

  1. 只是在複合操做(compound action)的整個執行過程當中(entire duration)持有一把鎖來維持state的原子性操做,是遠遠不夠的;而是應該在全部這個狀態可被獲取的地方(everywhere that variable is accessed)都用同一把鎖來協調對狀態的獲取(包括讀、寫)——可見性
  2. 全部(包含變量多於一個)的不定性,它所涉及的全部變量必須被同一把鎖保護。(For every invariant that involves more than one variable, all the variables
    involved in that invariant must be guarded by the same lock.)

                    活躍性與性能

                    1. 避免在較長時間的操做中持有鎖,例如網絡IO,控制檯IO等。
                    2. 在實現同步操做時,避免爲了性能而複雜化,可能會帶來安全性問題。

                    可見性

                    可見性比較難發現問題,是由於老是與咱們的直覺相違背。

                    重排序(reordering)的存在,易形成失效數據(Stale data),但這些數據多數都是以前某一個線程留下來的數據,而非隨機值,咱們稱這種狀況爲最低安全性(out-of-thin-air safety);但非原子的64位操做(如long,double),涉及到高位和低位分解爲2個32位操做的狀況,而沒法知足最低安全性,線程讀到的數據,多是線程A留下的高位和線程B留下的低位組合。除非用volatile關鍵字或鎖保護起來。

                    volatile關鍵字修飾的變量會避免與其餘內存操做重排序。慎用!

                    發佈Publishing與逸出escaped

                    發佈:使對象可以在當前做用域外被使用。

                    逸出:不該該發佈的對象被髮布時。

                    隱式this指針逸出問題:

                      1 public class ThisEscape {
                      2     private String name = null;
                      3 
                      4     public ThisEscape(EventSource source) {
                      5         source.registerListener(new EventListener() {
                      6             public void onEvent(Event event) {
                      7                 doSomething(event);
                      8             }
                      9         });
                     10         name = "TEST";
                     11     }
                     12 
                     13     protected void doSomething(Event event) {
                     14         System.out.println(name.toString());
                     15     }
                     16 }
                     17 // Interface
                     18 import java.awt.Event;
                     19 
                     20 public interface EventListener {
                     21     public void onEvent(Event event);
                     22 }
                     23 // class
                     24 public class EventSource {
                     25     public void registerListener(EventListener listener) {
                     26         listener.onEvent(null);
                     27     }
                     28 }
                     29 // Main
                     30 public class Client {
                     31     public static void main(String[] args) throws InterruptedException {
                     32         EventSource es = new EventSource();
                     33         new ThisEscape(es);
                     34     }
                     35 }
                    運行上述代碼會報空指針錯誤,是由於在name 初始化以前,就使用了ThisEscape實例(this指針逸出),而此時實例還沒有完成初始化。

                    修改以下,避免This逸出:

                      1 public class SafePublish {
                      2 
                      3     private final EventListener listener;
                      4     private String name = null;
                      5 
                      6     private SafePublish() {
                      7         listener = new EventListener() {
                      8             public void onEvent(Event event) {
                      9                 doSomething();
                     10             }
                     11         };
                     12         name = "TEST";
                     13     }
                     14 
                     15     public static SafePublish newInstance(EventSource eventSource) {
                     16         SafePublish safePublish = new SafePublish ();
                     17         eventSource.registerListener(safeListener.listener);
                     18         return safePublish;
                     19     }
                     20 
                     21     protected void doSomething() {
                     22         System.out.println(name.toString());
                     23     }
                     24 }

                    形成this指針逸出的狀況:

                    • 在構造函數中啓動了一個線程或註冊事件監聽;—私有構造器和共有工廠方法
                    • 在構造函數中調用一個能夠被override的方法(非private或final方法)

                    Thread confinement線程封閉

                    如Swing 和 JDBC的實現,使用局部變量(local variables )和 ThreadLocal 類

                    ad-hoc線程封閉:不太懂,就是開發者本身去維護封閉性?

                    Stack confinement棧封閉

                    不可變immutable

                    並非被final修飾的就是絕對的不可變!!

                    使用Volatile來發布不可變對象

                      1 @Immutable
                      2 class OneValueCache {
                      3 	private final BigInteger lastNumber;
                      4 	private final BigInteger[] lastFactors;
                      5 
                      6 	public OneValueCache(BigInteger i, BigInteger[] factors) {
                      7 		lastNumber = i;
                      8 		lastFactors = Arrays.copyOf(factors, factors.length);
                      9 	}
                     10 
                     11 	public BigInteger[] getFactors(BigInteger i) {
                     12 		if (lastNumber == null || !lastNumber.equals(i))
                     13 			return null;
                     14 		else
                     15 			return Arrays.copyOf(lastFactors, lastFactors.length);
                     16 	}
                     17 }
                     18 
                     19 // @ThreadSafe
                     20 public class VolatileCachedFactorizer implements Servlet {
                     21 	private volatile OneValueCache cache = new OneValueCache(null, null);
                     22 
                     23 	public void service(ServletRequest req, ServletResponse resp) {
                     24 		BigInteger i = extractFromRequest(req);
                     25 		BigInteger[] factors = cache.getFactors(i);
                     26 		if (factors == null) {
                     27 			factors = factor(i);
                     28 			cache = new OneValueCache(i, factors);
                     29 		}
                     30 		encodeIntoResponse(resp, factors);
                     31 	}
                     32 }
                     33 
                    相關文章
                    相關標籤/搜索