【J2SE】java併發程式設計實戰 讀書筆記( 一、二、三章)
執行緒的優缺點
執行緒是系統排程的基本單位。
執行緒如果使用得當,可以有效地降低程式的開發和維護等成本,同時提升複雜應用程式的效能。多執行緒程式可以通過提高處理器資源的利用率來提升系統的吞吐率。與此同時,線上程的使用開發過程中,也存在著諸多需要考慮的風險。
++i
執行緒安全性
執行緒安全的問題著重於解決如何對狀態訪問操作進行管理,特別是對共享和可變的狀態。共享意味著可多個執行緒同時訪問;可變即在變數在其生命週期內可以被改變;狀態就是由某個類中的成員變數(Field)。
一個無狀態的物件一定是執行緒安全的。因為它沒有可被改變的東西。
public class LoginServlet implements Servlet { public void service(ServletRequest req, ServletResponse resp) { System.out.println("無狀態Servlet,安全的類,沒有欄位可操作"); } }
原子性
正如我們熟知的++i
操作,它包含了三個獨立的“讀取-修改-寫入”操作序列,顯然是一個複合操作。為此java提供了原子變數來解決++i
這類問題。當狀態只是一個的時候,完全可以勝任所有的情況,但當一個物件擁有兩個及以上的狀態時,仍然存在著需要思考的複合操作,儘管狀態都使用原子變數。如下:
public class UnsafeCachingFactorizer implements Servlet { private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>(); private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>(); public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); if (i.equals(lastNumber.get())) { encodeIntoResponse(resp, lastFactors.get()); } else { BigInteger[] factors = factor(i); lastNumber.set(i); lastFactors.set(factors); encodeIntoResponse(resp, factors); } } } // lastNumber lastFactors 雖然都是原子的,但是 if-else 是複合操作,屬“先驗條件”
既然是複合操作,最直接,簡單的方式就是使用synchronized
將這個方法同步起來。這種方式能到達預期效果,但效率十分低下。
既然提到synchronized
加鎖同步,那麼就必須知道 鎖的特點:
- 鎖是可以重入的。即子類的同步方法可以呼叫本類或父類的同步方法。
- 同一時刻,只有一個執行緒能夠訪問物件中的同步方法。
- 靜態方法的鎖是 類;普通方法的鎖是 物件本身。
回顧上面的程式碼,一個方法體中,只要涉及了多個狀態的時候,就一定需要同步整個方法嗎?答案是否定的,同步只是為了讓多步操作為原子性,即對複合操作同步即可,因此需要明確的便是哪些操作是複合操作。如下:
public class CachedFactorizer implements Servlet { private BigInteger lastNumber; private BigInteger[] lastFactors; private long hits; private long cacheHits; public synchronized long getHits() { return hits; } public synchronized double getCacheHitRatio() { return (double) cacheHits / (double) hits; } public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = null; synchronized (this) { ++hits; if (i.equals(lastNumber)) { ++cacheHits; factors = lastFactors.clone(); } } if (factors == null) { factors = factor(i); synchronized (this) { lastNumber = 1; lastFactors = factors.clone(); } } encodeIntoResponse(reqsp, factors); } }// 兩個synchronized分別同步獨立的複合操作。
物件共享
重排序
:當一個執行緒修改物件狀態後,其他執行緒沒有看見修改後的狀態,這種現象稱為“重排序”。
java記憶體模型允許編譯器對操作順序進行重排序,並將資料快取在暫存器中。當缺乏同步的情況下,每一個執行緒在獨立的快取中使用快取的資料,並不知道主存中的資料已被更改。這就涉及到記憶體可見性的問題。
可見性
記憶體可見性
:同步的另一個重要的方面。我們不僅希望防止多個執行緒同時操作物件狀態,而且還希望確保某一個執行緒修改了狀態後,能被其他執行緒看見變化。
volatile
:使用synchronized
可以實現記憶體可見,但java提供了一種稍弱的更輕量級得同步機制volatile變數
。在訪問volatile變數時不會執行加鎖操作,因此不會產生執行緒阻塞。即便如此還是不能過度使用volatile
,當且僅當能簡化程式碼的實現以及對同步策略的驗證時,才考慮使用它。
釋出與逸出
釋出指:使物件能夠在當前作用於之外的程式碼中使用。即物件引用能被其他物件持有。釋出的物件內部狀態可能會破壞封裝性,使程式難以維持不變性條件。
逸出指:當某個不應該釋出的物件被髮布時,這種情況被稱為逸出。
// 正確釋出:物件引用放置公有靜態域中,所有類和執行緒都可見 class CarFactory { public static Set<Car> cars; private CarFactory() { cars = new HashSet<Car>(); }// 私有,外部無法獲取 CarFactory的引用 public static Car void newInstance() { Car car = new Car("碰碰車"); cars.put(car); return car; }// 使用方法來獲取 car }
// 逸出 class Person { private String[] foods = new String[] {"土豆"}; public Person(Event event) { person.registListener { new EventListener() { public void onEvent(Event e) { doSomething(e); } } } }// 隱式逸出了this,外界得到了Person的引用 並且 EventListener也獲取了Person的引用。 public String[] getFoods() { return foods; }// 對釋出的私有 foods,外界還是可以修改foods內部值 }
執行緒封閉
將可變的資料僅放置在單執行緒中操作的技術,稱之為發執行緒封閉。
棧封閉:只能通過區域性變數才能訪問物件。區域性變數的固有屬性之一就是封裝在執行執行緒中,它們位於執行執行緒的棧中,其他執行緒無法訪問這個棧,即只在一個方法內建立和使用物件。
public int test(Person p) { int num = 0; PersonHolder holder = new PersonHolder(); Person newPerson = deepCopy(p); Person woman = holder.getLove(newPerson); newPerson.setWomen(person); num++; return num; // 基本型別沒有引用,物件建立和修改都沒有逸出本方法 }
ThreadLocal類:ThreadLocal能夠使執行緒中的某個值與儲存值的物件關聯起來。ThreadLocal提供了get
、set
等訪問介面的方法,這些方法為每一個使用該變數的執行緒都存有一份獨立的副本,因get
總是返回由當前執行執行緒在呼叫set
時設定的最新值。
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() { public Connection initialValue() { return DriverManager.getConnection(DB_URL); } }; public static Connection getConnection() { return connectionHolder.get(); }
當某個頻繁執行的操作需要一個臨時物件,例如一個緩衝區,而同時又希望避免在每次執行時都重新分配該臨時物件,就可以使用ThreadLocal。
不變性
執行緒安全性是不可變物件的固有屬性之一。不可變物件一定是執行緒安全的
,它們的不變性條件是由建構函式建立的,只要它們的狀態不可變。
//在可變物件基礎上構建不可變類 public final class ThreadStooges { private final Set<String> stooges = new HashSet<String>(); public ThreadStooges() { stooges.add("Moe"); stooges.add("Larry"); } public boolean isStooge(String name) { return stooges.contains(name); } }// 沒有提供可修改狀態的方式,儘管使用了Set可變集合,但被private final修飾著
物件不可變的條件
- 物件建立以後其狀態就不能修改。
- 物件的所有域都是final型別。
- 物件是正確建立的(在物件的建立期間,this引用沒有逸出)
安全釋出
任何執行緒都可以在不需要額外同步的情況下安全地訪問不可變物件,即使在釋出這些物件時沒有使用同步。
// 安全的 Holder類 class Holder { private int n; public Holder(int n) { this.n = n; } } public class SessionHolder { // 錯誤的釋出,導致 Holder不安全 public Holder holder; public void init() { holder = new Holder(10); } }// 當初始化 holder的時候,holder.n會被先預設初始化為 0,然後建構函式才初始化為 10;在併發情況下,可能會有執行緒在預設初始化 與 構造初始化中,獲取到 n 值為 0, 而不是 10;
要安全的釋出一個物件,物件的引用以及物件的狀態必須同時對其他執行緒可見。一個正確構造的物件可以通過以下方式安全釋出:
- 在靜態初始化函式中初始化一個物件引用。
- 將物件的引用儲存到 volatitle 型別的域或者 AtomicReferance 物件中。
- 將物件的引用儲存到某個正確構造物件的 final 型別域中。
- 將物件的引用儲存到一個由鎖保護的域中。
線上程併發容器中的安全釋出:
- 通過將一個鍵或者值放入 Hashtable、synchronizedMap 或者 ConsurrentMap中,可以安全地將它釋出給任何從這些容器中訪問它的執行緒(無論是直接訪問還是通過迭代器訪問)。
- 通過將某個元素放入 Vector、 CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或 synchronizedSet中,可以將元素安全地釋出到任何從這些容器中訪問該元素的執行緒。
- 通過將某個元素放入 BlockingQueue或者ConcurrentLinkedQueue中,可以將該元素安全地釋出到任何從這些佇列中訪問該元素的執行緒。
通常,要釋出一個靜態構造的物件,最簡單、安全的方式就是使用靜態的初始化器。如public static Holder holder = new Holder(10)
。如果物件在釋出後狀態不會被修改(則稱為事實不可變物件),那麼在沒有額外的同步情況下,任何執行緒都可以安全地使用被安全釋出的不可變物件。
物件的釋出需求取決於它的可變性:
- 不可變物件可以通過任意機制來發布。
- 事實不可變物件必須通過安全方式來發布。
- 可變物件必須通過安全方式來發布,並且必須是執行緒安全的或者有某個鎖保護起來。
在併發程式中使用和共享物件時可採用的策略:
- 執行緒封閉。將物件封閉線上程中,如在方法中建立和修改區域性物件。
- 只讀共享。
- 執行緒安全共享。物件內部實現同步,使用公有介面來訪問。
- 保護物件。使用特定的鎖來保護物件。