參考文章:http://ifeve.com/java-concurrency-thread-directory/
其中的競態,線程安全,內存模型,線程間的通信,java ThreadLocal類小節部分內容。
- 1.目錄略覽
線程的基本概念:介紹線程的優點,代價,並發編程的模型。如何創建運行java 線程。
線程間通訊,共享內存的機制:
競態條件與臨界區,線程安全和共享資源與不可變性。java內存模型,線程間的通信,java ThreadLocal類,線程信號
死鎖相關,資源競爭相關:死鎖,如何避免死鎖,飢餓和公平,嵌套管程鎖死,Slipped conditions(從一個線程檢查某一特定條件到該線程操作此條件期間,這個條件已經被其它線程改變,導致第一個線程在該條件上執行了錯誤的操作),鎖,讀鎖和寫鎖,重入寫死,信號量,阻塞隊列,線程池,CAS(compare and swap 理論),同步器,無阻塞算法,阿姆達爾定律(計算處理器平行運算之后效率提升的能力)。
- 2.競態條件與臨界區
當多個線程訪問了相同的資源,並且對這些資源做了寫操作的時候,是不安全的。資源可以代表:同一內存區(變量,數組或者對象),系統(數據庫,web services)或文件。
對於一個簡單的加法操作 this.count = this.count + value,JVM執行指令的順序應該是:
從內存獲取 this.count 值放到寄存器
將寄存器的值添加value
將寄存器的值寫會內存
如果兩個線程 交叉執行,一個線程加2 一個線程加3,可能最后的結果不是5,而是2 或者3.
競態條件:兩個線程競爭同一個資源時,如果對資源訪問順序敏感,就存在競態條件。
臨界區:導致競態條件發生的代碼區成為臨界區。
在臨界區適當的同步可以避免競態條件。
- 3.線程安全與共享資源
允許被多個線程同時執行的代碼稱為線程安全的代碼。線程安全的代碼不包含競態條件。
1.局部變量
1.1局部基本類型變量 是存儲在線程自己的棧中的,所以基礎類型的局部變量是線程安全的。
1.2.局部對象引用 引用所指向的對象沒有存儲到線程的棧內。所有的對象都在共享堆中。
兩段代碼,不管是基礎類型還是引用對象,它們都是局部變量,由於都沒有被其他線程獲取,是線程安全的。
public void someMethod(){ long threadSafeInt = 0; threadSafeInt++; }
public void someMethod(){ LocalObject localObject = new LocalObject(); localObject.callMethod(); method2(localObject); } public void method2(LocalObject localObject){ localObject.setValue("value"); }
2.對象成員
對象成員是存儲在堆上。如果兩個線程同時更新同一個對象的同一成員,這個代碼就是線程不安全的。
public class NotThreadSage{ StringBuilder builder = New StringBuilder(); public add(String text) { this.builder.append(text); } }
線程控制逃逸判斷
一個資源的創建,使用銷毀都在同一個線程內完成,且永遠不會脫離該線程的控制。
即使對象本身線程安全,但是該對象中包含的其他的資源,也許整體的應用不是線程安全的。
3.線程安全及不可變性
immutable 和 read only 的差別:當一個變量是只讀的時候,變量的值不可改變,但是可以在其他變量發生改變的時候發生改變。而不變 是不會改變的。
- 4.java 內存模型
java內存模型規范了java虛擬機與計算機內存如何協同工作的。

每個java虛擬機的線程都擁有自己的線程棧,包括了這個線程調用的方法當前執行點的相關信息。一個線程只能訪問自己的線程棧。本地變量只對當前線程可見。

對象是放在堆上。
每個線程都有自己的線程棧,如果是基本類型的變量,直接存放在線程棧中,如果是對象的引用,那么引用地址會放在線程棧中,而對象會在堆中,這樣有可能存在兩個線程同時引用相同的對象。
public class MyRunnable implements Runnable() { public void run() { methodOne(); } public void methodOne() { int localVariable1 = 45; MySharedObject localVariable2 = MySharedObject.sharedInstance; //... do more with local variables. methodTwo(); } public void methodTwo() { Integer localVariable1 = new Integer(99); //... do more with local variable. } } public class MySharedObject { //static variable pointing to instance of MySharedObject public static final MySharedObject sharedInstance = new MySharedObject(); //member variables pointing to two objects on the heap public Integer object2 = new Integer(22); public Integer object4 = new Integer(44); public long member1 = 12345; public long member1 = 67890; }

兩個線程啟動后,Object3就是
MySharedObject,而Object2,Object4 是
MySharedObject中的 object2 和 Object4.
現代硬件內存架構

Java內存模型和硬件內存架構之間的橋接

硬件內存架構中沒有區分線程棧和堆。對於硬件所有線程棧和堆都是分布在主存中。部分線程棧和堆可能出現在CPU緩存和CPU內部的寄存器中。
當對象和變量被存放在計算機不同的內存區域中時,會有一些問題:
1.線程對共享變量修改的可見性。— 兩個線程分布運行在不同的CPU上時,線程的部分變量沒有刷新回主存,此時可能會導致不同步。可以使用 volatile 來避免。
2.當讀,寫和檢查共享變量時出現race conditions。多個線程同時修改共享內存的值,如下圖:

可以使用java同步塊,這樣同一時刻只能有一個線程可以進入代碼的臨界區。同步塊還可以保證代碼塊中所有被訪問的變量從主存中讀入,當線程退出同步塊時,所有被更新的變量也會被刷新回主存中,無論該變量是否被聲明為volatile.
5.java 同步塊
java同步塊 (synchronized block) 用來標記方法或者代碼塊是同步的。用來避免競爭。
java同步關鍵字:synchronized 所有其他等待進入該同步塊的線程將被阻塞,直到執行該同步塊的線程退出。
四種不同的同步塊:
實例方法;靜態方法;實例方法中的同步塊;靜態方法中的同步塊。——都是方法上的同步塊。
實例方法同步:
public synchronized void add(int value){ this.count += value; }
每個實例其方法同步都是同步在不同的對象上。這樣每個實例方法同步都同步在不同的對象上,即該方法所屬的實例,只有一個線程可以在實例方法同步塊中運行。一個實例一個線程。
靜態方法同步:
public static synchronized void add(int value){ count += value; }
靜態方法同步是指同步在該方法上所在的類對象上的。java虛擬機中一個類只能對應一個類對象,所以同時只允許一個線程執行同一個類中的靜態同步方法。不管類中的哪個靜態同步方法被調用,一個類只能由一個線程同時執行。
實例方法中同步塊:
public void add(int value){ synchronized(this){ this.count += value; } }
示例中使用的this 是代表的調用add方法的實例本身。在同步構造器中用括號括起來的對象叫做監視器對象。
靜態方法中同步塊:
public class MyClass { public static synchronized void log1(String msg1, String msg2){ log.writeln(msg1); log.writeln(msg2); } public static void log2(String msg1, String msg2){ synchronized(MyClass.class){ log.writeln(msg1); log.writeln(msg2); } } }
兩個方法不允許同時被線程訪問。
如果第二個同步塊不是同步在MyClass.class這個同步器上,這兩個方法可以同時被線程訪問。
java同步示例:
public class Counter{ long count = 0; public synchronized void add(long value){ this.count += value; } } public class CounterThread extends Thread{ protected Counter counter = null; public CounterThread(Counter counter){ this.counter = counter; } public void run() { for(int i=0; i<10; i++){ counter.add(i); } } } public class Example { public static void main(String[] args){ Counter counter = new Counter(); Thread threadA = new CounterThread(counter); Thread threadB = new CounterThread(counter); threadA.start(); threadB.start(); } }
由於兩個線程都是共用一個counter實例,所以add()被調用的時候是同步的,只有一個線程可以調用,另外一個需要等待。
public class Example { public static void main(String[] args){ Counter counterA = new Counter(); Counter counterB = new Counter(); Thread threadA = new CounterThread(counterA); Thread threadB = new CounterThread(counterB); threadA.start(); threadB.start(); } }
這個時候兩個線程就可以同時調用add()方法,因為它們分別在不同的實例中。
- 6.線程通信
線程通信的目的是使線程間可以互相發送信號。
方式:
1.通過共享對象通信
public class MySignal{ protected boolean hasDataToProcess = false; public synchronized boolean hasDataToProcess(){ return this.hasDataToProcess; } public synchronized void setHasDataToProcess(boolean hasData){ this.hasDataToProcess = hasData; } }
兩個線程獲得指向一個MySingal共享實例的引用,以便通信。同時獲取變量的方法設置為同步方法,防止線程不一致。
2.忙等待(Busy Wait)
protected MySignal sharedSignal = ... ... while(!sharedSignal.hasDataToProcess()){ //do nothing... busy waiting }
線程B一直在等待數據。但是感覺這里和前面獲取共享變量是一個原理。
3.wait(),notify()和 notifyAll()
wait()調用后就處於非運行狀態,直到另外一個線程調用了同一個對象的notify()方法。同時線程必須獲取這個對象的鎖才能調用。
public class MonitorObject{ } public class MyWaitNotify{ MonitorObject myMonitorObject = new MonitorObject(); public void doWait(){ synchronized(myMonitorObject){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } } public void doNotify(){ synchronized(myMonitorObject){ myMonitorObject.notify(); } } }
調用這個對象的notify() 的時候,有一個wait的線程會被隨機喚醒,同時也有一個notifyAll()方法來喚醒所有線程。
一旦線程調用了wait()方法,就釋放了所持有的監視器對象上的鎖,就允許了其他線程也可以調用wait()或者notify().同時一個線程被喚醒不是立刻就退出wait()的方法,直到調用notify()的線程退出了自己的同步塊。
4.丟失信號
由於notify()和notifyAll()不會保存調用它們的方法,他們發送的信號如果在wait()之前就有可能丟失,這個時候必須把他們保存在信號類里。
public class MyWaitNotify2{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ if(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
應該就是借助一個變量來記錄是否調用過Notify()。
5.假喚醒
有時由於莫名其妙的原因,線程可能在沒有掉用過notify()和 notifyAll()的情況下醒來。防止假喚醒,保存信號的成員變量會檢查是否是自己的信號,如果不是的話,就繼續wait()。
public class MyWaitNotify3{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
6.多個線程等待相同信號
while 循環也可以解決當多線程在等待時,只需要喚醒一個線程,並且是使用nitifyAll()來喚醒的情況。
7.不要在字符串常量或全局對象中調用wait()
就是導致假喚醒的原因之一,並且可能會導致信號沒有接收到。
管程 (Monitor)是對多個工作線程實現互斥訪問共享資源的對象和模塊。管程實現了在一個時間點,最多只有一個線程在執行他的某個子程序。
- 6 Java ThreadLocal
java中的ThreadLocal 可以讓變量只被同一個線程進行讀和寫操作。
創建:
private ThreadLocal myThreadLocal = new ThreadLocal()
訪問:
myThreadLocal.set(“local value”);
String threadLocalValue = (String) myThreadLocal.get();
如果不想用強制類型轉換,可以創建一個泛型化的ThreadLocal對象。
private ThreadLocal myThreadLocal1 = new ThreadLocal<String>();