[TOC]java
一個線程對共享變量的修改,另一個線程可以馬上看到,咱們稱爲可見性。算法
在單核的時代,不會出現問題。編程
多核時代,就會出現問題了。緩存
線程 A 操做的是 CPU-1 上的緩存,而線程 B 操做的是CPU-2上的緩存。線程A對變量V的操做對線程B不具有可見性了。這個就屬於硬件程序猿給軟件程序猿挖的坑。安全
如下代碼calc獲得的結果不會是20000。驗證了多核場景下的可見性問題。數據結構
public class Test { private long count = 0; private void add10K() { int idx = 0; while(idx++ < 10000) { count += 1; } } public static long calc() { final Test test = new Test(); // 建立兩個線程,執行 add() 操做 Thread th1 = new Thread(()->{ test.add10K(); }); Thread th2 = new Thread(()->{ test.add10K(); }); // 啓動兩個線程 th1.start(); th2.start(); // 等待兩個線程執行結束 th1.join(); th2.join(); return count; } }
咱們把一個或者多個操做在 CPU 執行的過程當中不被中斷的特性稱爲原子性併發
count+1,這個簡單的指令至少須要三條CPU指令。app
操做系統在任務切換,是能夠發生在任何一條CPU指令中間的,而不是在高級語言中的一句語句。函數
以下圖,線程A和線程B從CPU寄存器中讀出來的值都是0,並都是將1寫入到內存中。因此不會是咱們期待的2。性能
編譯器爲了優化性能,有時候會改變程序中語句的前後順序。
一個經典的案例,案例的雙重檢查鎖。
public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } }
當兩個線程同時執行這段代碼時,其中一個線程是有可能獲取到一個null的對象,訪問instance成員變量就會觸發空指針異常了。
問題出如今new這個動做上
咱們認爲的new動做應該是
實際上通過編譯後的優化,順序變成
在 32 位的機器上對 long 型變量進行加減操做存在併發。
確實會的,覺得long類型變量佔8位字節,也就是64位的,32位機器須要把變量拆分紅兩個32位操做。官方推薦把long/double變量聲明爲volatile或者使用同步鎖。
Java 內存模型規範了 JVM 如何提供按需禁用緩存和編譯優化的方法。
具體來講,這些方法包括volatile、synchronized、final三個關鍵字。以及六項Happens-before規則。
程序前面對某個變量的修改必定是對後續操做可見的。
對一個volatile變量的寫操做,Happens-before於後續對這個volatile變量的讀操做。
若是A Happens-before B,且B happens-before C,那麼A Happens-before C。
指對一個鎖的解鎖Happens before 於後續對這個鎖的加鎖。
也就是說必定要有解鎖動做,才能對這個鎖進行再次加鎖。
synchronized是java對管程的實現。
它指的是主線程A啓動子線程B後,子線程B可以看到主線程在啓動子線程B前的操做。
Thread B = new Thread(()->{ // 主線程調用 B.start() 以前 // 全部對共享變量的修改,此處皆可見 // 此例中,var==77 }); // 此處對共享變量 var 修改 var = 77; // 主線程啓動子線程 B.start();
它指的是主線程A等待子線程B完成,當子線程B完成後,主線程可以看到子線程的操做。
Thread B = new Thread(()->{ // 此處對共享變量 var 修改 var = 66; }); // 例如此處對共享變量修改, // 則這個修改結果對線程 B 可見 // 主線程啓動子線程 B.start(); B.join() // 子線程全部對共享變量的修改 // 在主線程調用 B.join() 以後皆可見 // 此例中,var==66
使用如下例子說明一下順序性、volatile變量規則,傳遞性。
class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { // 這裏 x 會是多少呢? } } }
final修飾變量時,初衷是告訴編譯器:這個變量生而不變,可使勁兒優化。
使用final的時候,只要提供正確的構造函數沒有「逸出」,就不會出問題。
一個關於逸出的例子,例子中經過global.obj讀取變量x是有可能讀取到0的。
final int x; // 錯誤的構造函數 public FinalFieldExample() { x = 3; y = 4; // 此處就是講 this 逸出, global.obj = this; }
class X { // 修飾非靜態方法 synchronized void foo() { // 臨界區 } // 修飾靜態方法 synchronized static void bar() { // 臨界區 } // 修飾代碼塊 Object obj = new Object(); void baz() { synchronized(obj) { // 臨界區 } } }
不一樣的資源用不一樣的鎖保護,各自管各自的。
用不一樣的鎖對受保護資源進行精細化管理,可以提高系能,這種鎖還有個名字,叫細粒度鎖。
class Account { // 鎖:保護帳戶餘額 private final Object balLock = new Object(); // 帳戶餘額 private Integer balance; // 鎖:保護帳戶密碼 private final Object pwLock = new Object(); // 帳戶密碼 private String password; // 取款 void withdraw(Integer amt) { synchronized(balLock) { if (this.balance > amt){ this.balance -= amt; } } } // 查看餘額 Integer getBalance() { synchronized(balLock) { return balance; } } // 更改密碼 void updatePassword(String pw){ synchronized(pwLock) { this.password = pw; } } // 查看密碼 String getPassword() { synchronized(pwLock) { return password; } } }
若是多個資源是有關聯關係的,那這個問題就比較複雜了。
class Account { private int balance; // 轉帳 synchronized void transfer( Account target, int amt){ if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } }
上面的代碼是錯誤的示例。
由於this這把鎖根本鎖不住target,也就是別人的帳戶。
會形成什麼問題呢?假設這樣的一個場景,ABC三個帳戶均爲200,一個線程執行A轉帳100給B,一個線程執行B轉帳100給C。最終致使的結果多是B爲300,或者B爲100。而咱們指望的結果B的餘額應該是200纔是正確的。
那麼其實解決方案也很是簡單,就是把鎖的對象this改爲Account.class
class Account { private int balance; // 轉帳 void transfer(Account target, int amt){ synchronized(Account.class) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } }
在上面一章中,咱們使用了Account.class鎖住了轉帳的動做,也就是說每筆轉帳動做,都是串行的動做了,性能降低嚴重,如何提高性能呢?
class Account { private int balance; // 轉帳 void transfer(Account target, int amt){ // 鎖定轉出帳戶 synchronized(this) { // 鎖定轉入帳戶 synchronized(target) { if (this.balance > amt) { this.balance -= amt; target.balance += amt; } } } } }
相比較於Account.class這個大鎖,咱們使用了兩個細粒度的鎖。
看起來很完美,可是極可能形成死鎖。
死鎖的一個比較專業定義是:一組互相競爭資源的線程因互相等待,致使永久阻塞的現象。
要解決死鎖,就要了解死鎖發生的條件。
Coffman牛人總結了爲四個條件:
1. 互斥,共享資源X和Y只能被一個線程佔用; 2. 佔有且等待,線程T1已經取得共享資源X,在等待共享資源Y的時候,不釋放共享資源X; 3. 不可搶佔,其餘線程不能強行搶佔線程T1佔有的資源; 4. 循環等待,線程T1等待線程T2佔有的資源,線程T2等待線程T1佔有的資源。
也就是說,咱們只要破壞掉其中一個條件,死鎖就迎刃而解了。
其中互斥性沒法破壞,由於咱們使用的就是互斥鎖。
破壞其他三個條件:
1. 破壞佔有等待,能夠一次性申請全部的資源 2. 破快不可搶佔,佔用部分資源的線程進一步申請其餘資源時,若是申請不到,主動釋放它佔有的資源 3. 破壞循環等待,能夠按序申請資源來預防。
class Allocator { private List<Object> als = new ArrayList<>(); // 一次性申請全部資源 synchronized boolean apply( Object from, Object to){ if(als.contains(from) || als.contains(to)){ return false; } else { als.add(from); als.add(to); } return true; } // 歸還資源 synchronized void free( Object from, Object to){ als.remove(from); als.remove(to); } } class Account { // actr 應該爲單例 private Allocator actr; private int balance; // 轉帳 void transfer(Account target, int amt){ // 一次性申請轉出帳戶和轉入帳戶,直到成功 while(!actr.apply(this, target)) ; try{ // 鎖定轉出帳戶 synchronized(this){ // 鎖定轉入帳戶 synchronized(target){ if (this.balance > amt){ this.balance -= amt; target.balance += amt; } } } } finally { actr.free(this, target) } } }
使用Lock,synchronzied沒法解決,後續討論。
增長id屬性,做爲排序屬性,鎖定順序按照從小到大的順序鎖定。
class Account { private int id; private int balance; // 轉帳 void transfer(Account target, int amt){ Account left = this ① Account right = target; ② if (this.id > target.id) { ③ left = target; ④ right = this; ⑤ } ⑥ // 鎖定序號小的帳戶 synchronized(left){ // 鎖定序號大的帳戶 synchronized(right){ if (this.balance > amt){ this.balance -= amt; target.balance += amt; } } } } }
既然解決死鎖有三個方法,那麼從三個方法中選擇出一個好的解決方法也顯得相當重要。
好比上面的例子,破壞佔用且等待條件成本就比破壞循環等待的成原本得高。
class Allocator { private List<Object> als; // 一次性申請全部資源 synchronized void apply( Object from, Object to){ // 經典寫法 while(als.contains(from) || als.contains(to)){ try{ wait(); }catch(Exception e){ } } als.add(from); als.add(to); } // 歸還資源 synchronized void free( Object from, Object to){ als.remove(from); als.remove(to); notifyAll(); } }
存在共享數據而且該數據會發生變化,通俗地說,就是多個線程會同時讀寫同一個數據。
若是多個線程同時寫同一個數據,這種狀況就稱之爲數據競爭。
除了死鎖,還有兩種狀況,分別是 活鎖和飢餓。
活鎖,線程雖然沒有發生阻塞,但仍然執行不下去的狀況。比如現實中的禮讓問題。
活鎖解決,加入嘗試等待一個隨機時間。
飢餓,線程因沒法訪問所需資源而沒法執行下去。
飢餓解決,通常使用公平鎖。
從方案層面,解決性能問題:
性能的度量指標有不少,通常三個很是重要,吞吐量、延遲、併發量。
public class BlockedQueue<T>{ final Lock lock = new ReentrantLock(); // 條件變量:隊列不滿 final Condition notFull = lock.newCondition(); // 條件變量:隊列不空 final Condition notEmpty = lock.newCondition(); // 入隊 void enq(T x) { lock.lock(); try { while (隊列已滿){ // 等待隊列不滿 notFull.await(); } // 省略入隊操做... // 入隊後, 通知可出隊 notEmpty.signal(); }finally { lock.unlock(); } } // 出隊 void deq(){ lock.lock(); try { while (隊列已空){ // 等待隊列不空 notEmpty.await(); } // 省略出隊操做... // 出隊後,通知可入隊 notFull.signal(); }finally { lock.unlock(); } } }
Java 語言中線程共有六種狀態,分別是:
看着比圖中多了幾個狀態,其實java中的BLOCED WAITING TIMED_WAITING都屬於休眠狀態,這個狀態下的線程沒有CPU的使用權。
最佳線程數 =CPU 核數 * [ 1 +(I/O 耗時 / CPU耗時)
每一個線程都有本身的調用棧,局部變量保存在線程各自的調用棧裏面,不會共享,因此局部變量不會有併發問題。