第11章:併發

第78條 對可以多個線程間共享的可變的數據的訪問進行同步

  1. 原子操作:指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch(切換到另一個線程),因此原子操作是不需要synchronized的
  2. synchronized的作用就是使一堆聚在一起的不具備原子性的代碼具備原子性
  3. 對象的不一致狀態(inconsistent state):指不同線程獲取的同一個對象,其屬性值可能不一致
  4. 互斥(mutual exclusion):當一個對象被線程A修改時,線程B無法觀察到這個對象,自然也無法觀察到這個在A、B線程中,狀態並不一致的對象

78.1 synchronized關鍵字作用

  1. 保證互斥、原子性
  2. 保證可見性(通信效果):保證進入同步方法或同步代碼塊的每個線程,都能看到由同一個鎖保護的、之前所有修改的最新效果

78.2 對可以多個線程間共享的可變數據的訪問不進行同步所帶來的問題

  1. java語言規範保證了,除了long和double變量,其他所有變量的讀、寫都是原子的,也就是說,即使多個線程在沒有同步的情況下,併發修改一個變量,讀取一個非long或double的變量的值時,這個值也一定是之前某個線程,寫到這個變量內的
  2. 對於long和double,因爲他們有8字節,64位,而32位的系統要讀或寫一個64位的變量,需要分兩步執行,每次讀或寫32位,因此long和double並不是原子的,即如果有兩個線程同時寫一個變量內存,一個進程寫低(前)32位,而另一個寫高(後)32位,這樣將導致獲取的64位數據是失效的數據。如果是在64位的系統中,那麼對64位的long和double的讀寫都是原子的
  3. 即使一個需要多個線程間共享的可變數據能夠進行原子性的讀寫,也要對他進行同步,否則後果非常可怕,因爲無法保證該數據在不同線程間可見
//該例想通過一個線程來中斷另一個線程
//此處想通過讓兩個線程共享一個變量stopRequested,當第一個線程修改這個變量值後,第二個線程發現該變量值變化,就自行停止,來實現功能
//注意此處不要使用Thread.stop方法實現該功能,這個功能本質上是不安全的,使用它會導致數據被破壞
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;
    }
}
//上面的程序永遠無法停止,因爲沒有同步,無法保證後臺線程具體什麼時候能看到主線程對stopRequested改變後的值
//既然無需保證什麼時候看到,那麼虛擬機就認爲可以將這段代碼,改寫成如下形式,從而進行優化,因爲他覺得stopRequested根本不會變,沒必要每次都查一下它的值
while (!stopRequested)
	i++;
if (!stopRequested)
	while (true)
		i++;
  1. 這種優化叫做hoisting(提升),OpenJDK Server VM也確實就是這樣做的
  2. 這種結果是一種活性失敗(liveness failure),程序並沒有得到改進
  3. 可以通過同步訪問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();
    }
}
  1. 注意變量的讀和寫方法都要進行同步,否則無法保證同步起作用,有時在某些機器上,只同步了寫或讀的程序也能正常工作,這只是表面現象
  2. 這裏的同步方法內的動作,即使沒有同步,也是原子的,所以這裏使用synchronized只是爲了保證通信效果
  3. 也可以使用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;
    }
}
  1. 注意volatile無法保證互斥(原子性)
private static volatile int nextSerialNumber = 0;
//該方法想每次調用都返回一個新的值,但由於++操作並不是原子的,++會先讀取變量值,然後寫回一個新值
//如果第二個線程在第一個線程讀取舊值和寫回新值之間讀取這個域,第二個線程就會與第一個線程一起看到同一個值,並返回相同的序列號,這叫做安全性失敗(safety failure)
public static int generateSerialNumber() {
	return nextSerialNumber++;
}
  1. 可以通過使用synchronized修飾generateSerialNumber方法,來解決這個問題
//已經使用了synchronized關鍵字,就也沒必要在使用volatile了
private static int nextSerialNumber = 0;
public synchronized static int generateSerialNumber() {
    return nextSerialNumber++;
}
  1. 還可以通過使用AtomicLong類來解決這個問題,AtomicLong類來自於java.util.concurrent.atomic包,這個包內提供了,基本類型的無需鎖定就能保證線程安全的版本的類,這個包內的類,不止具有原子性,還能保證通信效果(volatile的作用),它可以做的比synchronized更好
private static final AtomicLong nextSerialNum = new AtomicLong();
public static long generateSerialNumber() {
	return nextSerialNum.getAndIncrement();
}

78.3 避免共享可變數據的四種方法

  1. 將可變數據改爲不可變數據
  2. 壓根不再進行共享:將可變數據限制在單個線程中。如果這樣做了,需要對這個可變數據建立文檔,表示不要將它放到多個線程中使用
  3. 防止共享了自己都不知道:應深刻理解正在使用的框架和類庫,因爲它們可能引入了你根本不知道的線程對數據進行處理
  4. 對高效不可變數據進行安全發佈(不可變數據隨意發佈,可變數據安全發佈+讀寫同步)

78.4 安全發佈

  1. 發佈:使對象能夠在當前作用域之外的代碼中使用
//1. 使用這個類.student就是將student發佈
public Student student;

//2. 使用當前類的對象的getStudent方法, 也叫發佈
public Student getStudent(){
	return student;
}
  1. 安全發佈:保證發佈出去的對象的引用和狀態同時對其他線程可見的發佈技術
public class Publish {
    //使用Publish.holder將holder對象傳遞給其他方法,就是將holder不安全地發佈了
    //此處爲不安全的發佈,是因爲,無法保證holder在多個不同線程間的可見性
    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.");
    }
}
  1. 安全發佈的常用模式,也就是能保證對象的引用和狀態同時對其他線程可見的幾種方式。我理解安全發佈是爲保證可見性存在的,而指令重排序是引起可見性的其中一個原因
    1. 在靜態初始化函數中,初始化一個對象的引用:public static Holder holder = new Holder(42)
    2. 將對象引用保存在volatile類型的屬性或AtomicReference對象中
    3. 將對象的引用保存在某個正確構造對象的final類型域中
    4. 將對象的引用保存在一個由鎖保護的域中,這條就是effective java中所謂的"讓一個線程短時間內修改一個數據對象,然後通過只同步共享對象引用的動作,將數據與其他線程共享(安全發佈),是可以接受的"
    5. 將對象保存在併發的集合中

78.5 高效不可變對象(effectively immutable)

  1. 不可變對象:對象的屬性全用final修飾,同時屬性所引用的對象也是不可變的
  2. 高效不可變對象:實際上就是某個對象,不滿足不可變對象的要求,但這個對象確實沒人去改變它的屬性值,導致它確實從來不會變

78.6 最佳實踐

  1. 多個線程共享可變數據時,每個讀或寫這個數據的線程,都應該執行同步
  2. 不同步共享可變數據,會造成程序的活性失敗和安全性失敗,這樣的失敗很難調試(發現),這種失敗可能是間歇性的,且與時間相關的,而且程序的行爲在不同虛擬機上也不同
  3. 如果只需要線程間的交互通信,而不需要互斥,可以使用volatile替代synchronized完成同步
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章