第78條 對可以多個線程間共享的可變的數據的訪問進行同步
- 原子操作:指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch(切換到另一個線程),因此原子操作是不需要synchronized的
- synchronized的作用就是使一堆聚在一起的不具備原子性的代碼具備原子性
- 對象的不一致狀態(inconsistent state):指不同線程獲取的同一個對象,其屬性值可能不一致
- 互斥(mutual exclusion):當一個對象被線程A修改時,線程B無法觀察到這個對象,自然也無法觀察到這個在A、B線程中,狀態並不一致的對象
78.1 synchronized關鍵字作用
- 保證互斥、原子性
- 保證可見性(通信效果):保證進入同步方法或同步代碼塊的每個線程,都能看到由同一個鎖保護的、之前所有修改的最新效果
78.2 對可以多個線程間共享的可變數據的訪問不進行同步所帶來的問題
- java語言規範保證了,除了long和double變量,其他所有變量的讀、寫都是原子的,也就是說,即使多個線程在沒有同步的情況下,併發修改一個變量,讀取一個非long或double的變量的值時,這個值也一定是之前某個線程,寫到這個變量內的
- 對於long和double,因爲他們有8字節,64位,而32位的系統要讀或寫一個64位的變量,需要分兩步執行,每次讀或寫32位,因此long和double並不是原子的,即如果有兩個線程同時寫一個變量內存,一個進程寫低(前)32位,而另一個寫高(後)32位,這樣將導致獲取的64位數據是失效的數據。如果是在64位的系統中,那麼對64位的long和double的讀寫都是原子的
- 即使一個需要多個線程間共享的可變數據能夠進行原子性的讀寫,也要對他進行同步,否則後果非常可怕,因爲無法保證該數據在不同線程間可見
public class StopThread {
private static boolean stopRequested;
public static void main(String[] args)
throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested)
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
while (!stopRequested)
i++;
if (!stopRequested)
while (true)
i++;
- 這種優化叫做hoisting(提升),OpenJDK Server VM也確實就是這樣做的
- 這種結果是一種活性失敗(liveness failure),程序並沒有得到改進
- 可以通過同步訪問stopRequested屬性,來解決這個問題,下面的程序和期待的一樣,一秒後就停止
public class StopThread {
private static boolean stopRequested;
private static synchronized void requestStop() {
stopRequested = true;
}
private static synchronized boolean stopRequested() {
return stopRequested;
}
public static void main(String[] args)
throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested())
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
requestStop();
}
}
- 注意變量的讀和寫方法都要進行同步,否則無法保證同步起作用,有時在某些機器上,只同步了寫或讀的程序也能正常工作,這只是表面現象
- 這裏的同步方法內的動作,即使沒有同步,也是原子的,所以這裏使用synchronized只是爲了保證通信效果
- 也可以使用volatile關鍵字來保證通信效果,它可以保證任何一個線程在讀取該屬性時(volatile只能修飾屬性,不能修飾局部變量),都將看到最近剛剛被寫入的值
public class StopThread {
private static volatile boolean stopRequested;
public static void main(String[] args)
throws InterruptedException {
Thread backgroundThread = new Thread(() -> {
int i = 0;
while (!stopRequested)
i++;
});
backgroundThread.start();
TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}
- 注意volatile無法保證互斥(原子性)
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
return nextSerialNumber++;
}
- 可以通過使用synchronized修飾generateSerialNumber方法,來解決這個問題
private static int nextSerialNumber = 0;
public synchronized static int generateSerialNumber() {
return nextSerialNumber++;
}
- 還可以通過使用AtomicLong類來解決這個問題,AtomicLong類來自於java.util.concurrent.atomic包,這個包內提供了,基本類型的無需鎖定就能保證線程安全的版本的類,這個包內的類,不止具有原子性,還能保證通信效果(volatile的作用),它可以做的比synchronized更好
private static final AtomicLong nextSerialNum = new AtomicLong();
public static long generateSerialNumber() {
return nextSerialNum.getAndIncrement();
}
78.3 避免共享可變數據的四種方法
- 將可變數據改爲不可變數據
- 壓根不再進行共享:將可變數據限制在單個線程中。如果這樣做了,需要對這個可變數據建立文檔,表示不要將它放到多個線程中使用
- 防止共享了自己都不知道:應深刻理解正在使用的框架和類庫,因爲它們可能引入了你根本不知道的線程對數據進行處理
- 對高效不可變數據進行安全發佈(不可變數據隨意發佈,可變數據安全發佈+讀寫同步)
78.4 安全發佈
- 發佈:使對象能夠在當前作用域之外的代碼中使用
public Student student;
public Student getStudent(){
return student;
}
- 安全發佈:保證發佈出去的對象的引用和狀態同時對其他線程可見的發佈技術
public class Publish {
public Holder holder;
public void initialize() {
holder = new Holder(42);
}
}
class Holder {
int n;
public Holder(int n) {
this.n = n;
}
public void assertSanity() {
if (n != n)
throw new AssertionError("This statement is false.");
}
}
- 安全發佈的常用模式,也就是能保證對象的引用和狀態同時對其他線程可見的幾種方式。我理解安全發佈是爲保證可見性存在的,而指令重排序是引起可見性的其中一個原因
- 在靜態初始化函數中,初始化一個對象的引用:public static Holder holder = new Holder(42)
- 將對象引用保存在volatile類型的屬性或AtomicReference對象中
- 將對象的引用保存在某個正確構造對象的final類型域中
- 將對象的引用保存在一個由鎖保護的域中,這條就是effective java中所謂的"讓一個線程短時間內修改一個數據對象,然後通過只同步共享對象引用的動作,將數據與其他線程共享(安全發佈),是可以接受的"
- 將對象保存在併發的集合中
78.5 高效不可變對象(effectively immutable)
- 不可變對象:對象的屬性全用final修飾,同時屬性所引用的對象也是不可變的
- 高效不可變對象:實際上就是某個對象,不滿足不可變對象的要求,但這個對象確實沒人去改變它的屬性值,導致它確實從來不會變
78.6 最佳實踐
- 多個線程共享可變數據時,每個讀或寫這個數據的線程,都應該執行同步
- 不同步共享可變數據,會造成程序的活性失敗和安全性失敗,這樣的失敗很難調試(發現),這種失敗可能是間歇性的,且與時間相關的,而且程序的行爲在不同虛擬機上也不同
- 如果只需要線程間的交互通信,而不需要互斥,可以使用volatile替代synchronized完成同步