《Java 線程編程》學習筆記7
第七章 併發訪問對象和變量
- 當多個線程與對象交互時,則需要適當的控制,以確保線程間不會產生不利的影響。
7.1 易變成員變量修飾符
volatile
關鍵字是用於成員變量的一個修飾符,每次訪問線程時,強迫它從共享內存中重讀變量的值。而且,當變量發生變化時,強迫線程將變化值寫到共享內存中。如此一來,不管在任意時刻,兩個不同的線程總是看到某個成員變量的同一個值。- Java 語言規範表明,爲了獲得最佳速度,允許線程保存共享成員變量的工作拷貝,而且只是偶爾用共享的原始值來校準。爲了更準確地描述,“偶爾”可以翻譯爲:“當線程進入或離開同步校驗代碼塊時”。
volatile
關鍵字用於告訴 VM:它不應該保存變量的私有拷貝,而應該直接與共享拷貝交互。- 代碼示例:
public class Volatile extends Object implements Runnable {
// 沒有標記爲 volatile,但是應該標記的
private int value;
private volatile boolean missedIt;
// 不需要申明爲 volatile 的變量
private long creationTime;
public Volatile() {
value = 10;
missedIt = false;
creationTime = System.currentTimeMillis();
}
public void run() {
print("entering run()");
// 每次檢查 value 值是否相同
while(value < 20) {
// 如果找不到對值修改,則跳出循環
if(missedIt) {
int currValue = value;
// 在一個對象上執行同步語句,觀察其效果
Object lock = new Object();
synchronized(lock) {
// 不做任何事情
}
int valueAfterSync = value;
print("in run() - see value = " + currValue + ", but rumor has it that it changed!");
print("in run() - valueAfterSync = " + valueAfterSync);
break;
}
}
print("leaving run()");
}
private void workMethod() throws InterruptedException {
print("entering workMethod()");
print("in workMethod() - about to sleep for 2 second");
Thread.sleep(2000);
value = 50;
print("in workMethod() - just set value = " + value);
print("in workMethod() - about to sleep for 5 second");
Thread.sleep(5000);
missedIt = true;
print("in workMethod() - just set missdIt = " + missedIt);
print("in workMethod() - about to sleep for 3 second");
Thread.sleep(3000);
print("leaving workMethod()");
}
private void print(String msg) {
// 使用 java.text 包的功能
// 可以簡化這個方法
// 但這裏沒有利用這一點
// 因爲 JDK1.0 沒有這個包
...
}
public static void main(String[] args) {
try {
Volatile vol = new Volatile();
Thread.sleep(100);
Thread t = new Thread(vol);
t.start();
Thread.sleep(100);
vol.workMethod();
} catch(InterruptedException x) {
System.out.println("one of the sleeps was interrupted.");
}
}
}
/*
執行的可能結果:
Thead-0: entering run()
main: entering workMethod()
main: in workMethod() - about to sleep for 2 second
main: in workMethod() - just set value = 50
main: in workMethod() - about to sleep for 5 second
main: in workMethod() - just set missedIt = true
main: in workMethod() - about to sleep for 3 second
Thread-0: in run() - see value = 10, but rumor has it that it changed!
Thread-0: in run() - valueAfterSync = 50
Thread-0: leaving run()
main: leaving workMethod()
加上 volatile 後可能的執行結果:
Thead-0: entering run()
main: entering workMethod()
main: in workMethod() - about to sleep for 2 second
main: in workMethod() - just set value = 50
main: in workMethod() - about to sleep for 5 second
Thread-0: leaving run()
main: in workMethod() - just set missedIt = true
main: in workMethod() - about to sleep for 3 second
main: leaving workMethod()
*/
- Sun 在 VM 中包含 JIT 之前,使用
volatile
無差異。(JIT = Just-in-time 及時編譯技術)
什麼是JIT?
參考:http://blog.csdn.net/ns_code/article/details/18009455
不論是物理機還是虛擬機,大部分的程序代碼從開始編譯到最終轉化成物理機的目標代碼或虛擬機能執行的指令集之前,都會按照如下圖所示的各個步驟進行:
其中綠色的模塊可以選擇性實現。很容易看出,上圖中間的那條分支是解釋執行的過程(即一條字節碼一條字節碼地解釋執行,如JavaScript),而下面的那條分支就是傳統編譯原理中從源代碼到目標機器代碼的生成過程。
如今,基於物理機、虛擬機等的語言,大多都遵循這種基於現代經典編譯原理的思路,在執行前先對程序源碼進行詞法解析和語法解析處理,把源碼轉化爲抽象語法樹。對於一門具體語言的實現來說,詞法和語法分析乃至後面的優化器和目標代碼生成器都可以選擇獨立於執行引擎,形成一個完整意義的編譯器去實現,這類代表是C/C++語言。也可以把抽象語法樹或指令流之前的步驟實現一個半獨立的編譯器,這類代表是Java語言。又或者可以把這些步驟和執行引擎全部集中在一起實現,如大多數的JavaScript執行器。
7.2 同步方法修飾符
- 在方法中添加修飾符
synchronized
,確保在同一時刻,方法內只允許有一個線程,當對象的狀態臨時處於不一致時,這對於阻止其他線程進入方法有用。
7.2.1 兩個線程同時位於一個對象的同一個方法中(沒加 synchronized)
7.1.1 同一時刻一個線程(加 synchronized)
- 當線程碰到
synchronized
實例方法時,就會一直阻塞到可以排它性訪問對象級別的互斥鎖(mutex lock)爲止。互斥(mutex)是互相排斥(mutual exclusion)的縮寫。互斥鎖在一個時刻只能由一個線程持有。當釋放該鎖時,所有等待的線程均競爭排它性訪問權限。只有一個線程可以競爭成功,其它線程恢復阻塞狀態,並再次等待鎖的釋放。 - 如果對象上的一個
synchronized
方法調用同一個對象上的另一個synchronized
方法,它不會阻塞來競爭對象級別的鎖(還有其他級別的鎖?),因爲它已經獲得了排它性訪問鎖的權限。
疑問:除了對象級別鎖,還有其他級別的鎖?
參考:http://zhh9106.iteye.com/blog/2151791
在java編程中,經常需要用到同步,而用得最多的也許是synchronized關鍵字了,下面看看這個關鍵字的用法。因爲synchronized關鍵字涉及到鎖的概念,所以先來了解一些相關的鎖知識。
java的內置鎖:每個java對象都可以用做一個實現同步的鎖,這些鎖成爲內置鎖。線程進入同步代碼塊或方法的時候會自動獲得該鎖,在退出同步代碼塊或方法時會釋放該鎖。獲得內置鎖的唯一途徑就是進入這個鎖的保護的同步代碼塊或方法。java內置鎖是一個互斥鎖,這就是意味着最多隻有一個線程能夠獲得該鎖,當線程A嘗試去獲得線程B持有的內置鎖時,線程A必須等待或者阻塞,知道線程B釋放這個鎖,如果B線程不釋放這個鎖,那麼A線程將永遠等待下去。
**java的對象鎖和類鎖:**java的對象鎖和類鎖在鎖的概念上基本上和內置鎖是一致的,但是,兩個鎖實際是有很大的區別的,對象鎖是用於對象實例方法,或者一個對象實例上的,類鎖是用於類的靜態方法或者一個類的class對象上的。我們知道,類的對象實例可以有很多個,但是每個類只有一個class對象,所以不同對象實例的對象鎖是互不干擾的,但是每個類只有一個類鎖。但是有一點必須注意的是,其實類鎖只是一個概念上的東西,並不是真實存在的,它只是用來幫助我們理解鎖定實例方法和靜態方法的區別的
public class TestSynchronized
{
public synchronized void test1()
{
int i = 5;
while( i-- > 0)
{
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
}
catch (InterruptedException ie)
{}
}
}
public static synchronized void test2()
{
int i = 5;
while( i-- > 0) {
System.out.println(Thread.currentThread().getName() + " : " + i);
try {
Thread.sleep(500);
}
catch (InterruptedException ie)
{}
}
}
public static void main(String[] args)
{
final TestSynchronized myt2 = new TestSynchronized();
Thread test1 = new Thread( new Runnable() { public void run() { myt2.test1(); } }, "test1" );
Thread test2 = new Thread( new Runnable() { public void run() { TestSynchronized.test2(); } }, "test2" );
test1.start();
test2.start();
// TestRunnable tr=new TestRunnable();
// Thread test3=new Thread(tr);
// test3.start();
}
}
/*
上面代碼synchronized同時修飾靜態方法和實例方法,但是運行結果是交替進行的,這證明了類鎖和對象鎖是兩個不一樣的鎖,控制着不同的區域,它們是互不干擾的。同樣,線程獲得對象鎖的同時,也可以獲得該類鎖,即同時獲得兩個鎖,這是允許的。
*/
7.2.3 兩個線程,兩個對象
- 類的每個對象都有自己的對象級別鎖!
7.2.4 避免對象的意外崩潰
- 對於原子操作,一般是不存在線程問題的。
- 但是,如果兩個線程同時對變量進行賦值,這種非原子操作,就可能出現線程問題。
public class CorruptWrite extends Object {
private String fname;
private String lname;
public void setNames(String firstName, String lastName) {
print("entering setName()");
fname = firstName;
// 線程可能從此處交換出去,
// 可能在外部逗留不同的時間,
// 用不同的休眠時間放大了該值
if(fname.length() < 5) {
try {Thread.sleep(1000);}
catch(InterruptedException x) {}
}
else {
try {Thread.sleep(2000);}
catch(InterruptedException x) {}
}
lname = lastName;
print("leaving setName() - " + lname + ", " + fname);
}
public static void print(String name) {
String threadName = Thread.currentThread().getName();
System.out.println(threadName + ": " + msg);
}
public static void main(String[] args) {
final CorruptWrite cw = new CorruptWrite();
Runnable runA = new Runnable() {
public void run() {
cw.setName("George", "Washington");
}
};
Thread threadA = new Thread(runA, "threadA");
threadA.start();
try {Thread.sleep(200)};
catch(InterruptedException x) {}
Runnable runB = new Runnalbe() {
public void run() {
cw.setNames("Abe", "Lincoln");
}
}
Thread threadB = new Thread(runB, "threadB");
threadB.start();
}
}
- 在多線程環境中,以上修改後的
setName()
方法快速版本仍然存在不易察覺的危險。可能剛好在 fname 賦值之後,lname 賦值之前,線程規劃器讓 threadA 從處理器中交換出去。雖然 threadA 只完成了一半的工作,但 threadB 可能交換進來,並將它的參數賦值給 fname 和 lname。這就使得對象處於不一致狀態。這段代碼大部分時間可以良好運行,但是偶爾會出現崩潰現象。 - 給代碼添加
synchronized
屬性後可以避免這個問題。讓setNames()
成爲一個synchronized方法,則試圖進入該方法的所有線程都將阻塞,直到獲得對象級別鎖的排斥訪問權限爲止。
7.2.5 對象處於不一致狀態時,推遲對它的訪問
- 7.2.4 中添加
synchronized
關鍵字使得對變量的修改保持了唯一性,現在我們考慮變量的讀取的唯一性。 - 代碼如下:
public class DirtyRead extends Object {
private String fname;
private String lname;
public String getNames() {
return lname + ", " + fname;
}
public synchronized void setNames(String firstName, String lastName) {
...
}
public static void main(String[] args {
final DirtyRead dr = new DirtyRead();
dr.setNames("George", "Washington");
Runnable runA = new Runnable {
public void run() {
dr.setNames("Abe", "Lincoln");
}
}
try {Thread.sleep(200);}
catch(InterruptedException x) {}
Runnable runB = new Runnable {
public void run() {
print("getName() = " + dr.getnames();
}
}
Thread threadB = new Thread(runB, "threadB");
threadB.start();
}
}
/*
輸出結果:
main: entering setNames()
main: leaving setNames() - Washington, George
threadA: entering setNames()
threadB: getNames() = Washington, Abe
threadA: leaving setNames() - Lincoln, Abe
*/
- 上述例子說明了一個不可避免的事實:對象必定在短時間內處於不一致的狀態,即使只是保留賦值,刪除其他所有語句,情況也是如此。
- 不論處理器速度多快,線程規劃器也可能交換出進行更改的線程,交換它的時間在修改 fname 之後,但在修改 lname 之前。持有一個對象級別鎖不會阻止線程被交換出來。如果被交換出來,它將繼續持有對象級別鎖。因此,必須小心數據處於不一致狀態時,確保阻塞所有的讀。
- 下面的代碼通過在
getNames()
方法上添加一個synchronized
關鍵字來控制併發讀和寫。
/*
由於 getNames() 是 synchronized 方法,所以,處於 setNames() 狀態下時,getNames() 的 threadB 阻塞,儘量獲得對對象級別鎖的排斥訪問權限。當 threadA 從 setNames() 方法退出後,自動釋放對象級別鎖。於是,threadB 得以獲得對象級別鎖,並進入 getNames() 方法。
*/
public synchronized String getNames() {
return lname + ", " + fname;
}
技巧:
如果兩個或更多線程同時與某個對象的成員變量交互,而且至少其中一個線程會更改它的值,則一般來說,理想的做法是使用synchronized
來控制併發訪問。如果只有一個線程訪問對象,那麼,沒有必要使用 synchronized,這樣反而會減緩它的執行速度。(從這裏也可以看出,對於一個對象,只有一個對象鎖。)
7.3 同步語句塊
- 當整個方法不需要同步,或者希望線程獲得不同對象上的對象級別鎖時,可以使用同步塊(synchronized block)。同步(synchronized)語句塊如下所示。
synchronized (obj) {
// 代碼塊
}
7.3.1 減少持有鎖的時間
synchronized
塊可以用於減少持有對象級別鎖的時間。如果方法進行大量其他不需要訪問成員變量的工作,就可以縮短持有鎖的時間,只限制到關鍵的部分:
public void setValues(int x, double ratio) {
// 它們不需要使用成員變量
// ...
double processedValA = ... // 長時間計算
double processedValB = ... // 長時間計算
// ...
synchronized (this) {
a = processedValA;
b = processedValB;
}
}
7.3.2 鎖定任意對象,而非僅僅鎖定當前對象
- 同步語句:
/*
mutex 是 VM 中任意對象
*/
syncrhonized(mutex) {
}
7.3.3 把向量內容安全地複製到數組
- 以 Vectory 類爲例進行復制,這個可以推廣到所有的複製/添加內容方法。
Vector vect = new Vector();
synchronized(vect) {
int size = vect.size();
word = new String[size];
for(int i = 0; i < word.length; i++) {
word[i] = (String) vect.elementAt(i);
}
}
警告:
從 JDK1.2 開始,Vector 和 Hashtable 已經添加到 Collections API 中。所欲的方法仍然存在,同時還添加了一些新的非同步方法。示例只對 JDK1.2 以前安全。
7.4 靜態同步方法
- 對於類的每個實例,除了存在對象級別鎖外,還存在類級別鎖,它被特定類的所有實例共享。VM 裝載的每個類只有一個類級別鎖。如果方法既是靜態,又是同步的,則線程進入方法前,必須獲取排斥性訪問類級別鎖的權限。
7.5 在同步語句中使用類級別鎖
- 示例代碼:
synchronized (ClassName.class) {
// 方法體
}
7.6 同步化和集合API
- JDK1.2 以後,Collection API 是新添加的,其中包含大量接口和類。
7.6.1 封裝集合,使之同步化
- 最初設計的 Vector 和 HashTable 是多線程安全的。例如,對於 Vectory 而言,刪除或添加元素的方法是同步的。等等。
- 集合 API 的設計者希望避免在不必要的時候濫用同步化,以免帶來過多的死鎖。因此,對於更改集合內容的方法,沒有一個是同步化的。如果多線程訪問集合(Collection)或映射(Map),則應當用一個同步化所有方法的類封裝它。
警告:
集合本質上非多線程安全。當多個線程與集合交互時,爲了使它多線程安全,必須採取額外的措施。
- 在 Collection 類中有多個靜態方法,他們用於運用同步方法封裝非同步集合,例如:
List list = Collections.synchronizedList(new ArrayList());
技巧:
當同步化集合時,不要使用原始未同步集合的直接引用。這將確保其他線程不會意外作出不一致的更改。
7.6.2 安全地把列表中的內容複製到數組
- 下面展示了3種安全的途徑:
...
// 爲了安全起見,僅使用同步列表的一個引用
// 可以確保控制了所有的訪問
List wordList = Collections.synchronizedList(new ArrayList());
// 第一種技術(推薦)
String[] wordA = (String[]) wordList.toArray(new String[0])
// 第二種技術
String[] wordB;
synchronized(wordList) {
int size = wordList.size();
wordB = new String[size];
wordList.toArray(wordB);
}
// 第三種技術(必須使用 synchronized)
String[] wordC;
synchronized (wordList) {
wordC = (String[]) wordList.toArray(new String[wordList.size()]);
}
...
7.6.3 安全遍歷集合元素
7.7 死鎖
- 死鎖出現情況的抽象圖:
7.7.1 規避死鎖
- 對於容易發生死鎖的代碼,應該儘量遵循以下原則:
- 只在必要的最短時間內持有鎖。考慮使用同步語句塊代替整個同步方法。
- 儘量編寫不在同一時刻需要持有多個鎖的代碼。如果不可避免,則確保線程持有第二個鎖的時間儘量短。
- 創建和使用一個大鎖來代替多個小鎖。
7.8 加速併發訪問
- 同步對於編寫多線程代碼十分關鍵。但是同步需要付出代價。獲取和釋放鎖的簡單任務給處理器添加了更多的工作量,因而減慢了執行速度。這個額外的開銷就是爲什麼默認時,集合 API 中的方法不是同步方法阿德原因。只有一個線程處理集合時,同步化是一種處理器資源的浪費。