線程安全,底層實現原理和JMM

讓多線程下的類安全起來:無狀態、加鎖、讓類不可變、棧封閉(方法封裝) 、安全的發佈對象(不暴露成員)java

死鎖

必定發生在多個線程爭奪多個資源裏的狀況下,發生的緣由是每一個線程拿到了某個(某些)資源不釋放,同時等待着其餘線程所持有的資源。解決死鎖的原則就是確保正確的獲取資源的順序,或者獲取資源時使用定時嘗試機制。程序員

常見的死鎖:簡單順序死鎖、動態順序死鎖編程

/**
 *簡單順序死鎖
 */
public class SimpleDeadLock {

    private static Object left = new Object();
    private static Object right = new Object();

    //先鎖左 再右
    private static void leftToRight() throws InterruptedException {
        synchronized (left){
            System.out.println(Thread.currentThread().getName()+" get left");
            Thread.sleep(100);
            synchronized (right){
                System.out.println(Thread.currentThread().getName()+" get right");
            }
        }
    }

    //先鎖左 再右(若是相反就會出現順序死鎖)
    private static void rightToLeft() throws InterruptedException {
        synchronized (left){
            System.out.println(Thread.currentThread().getName()+" get right-left");
            Thread.sleep(100);
            synchronized (right){
                System.out.println(Thread.currentThread().getName()+" get left-right");
            }
        }
    }

    private static class TestThread extends Thread{
        private String name;

        public TestThread(String name) {
            this.name = name;
        }

        @Override
        public void run(){
            try {
                rightToLeft();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Thread.currentThread().setName("Main");
        TestThread  testThread = new TestThread("testThread");
        testThread.start();
        try {
            leftToRight();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}
=========== 
運行結果  
死鎖時=============
Main get left
Thread-0 get right-left
......

正常時=====================
Main get left
Main get right
Thread-0 get right-left
Thread-0 get left-right

動態順序死鎖示例數組

/**
 * 賬號信息類
 */
public class Account {
    private long number;
    private final String name;
    private int money;
    private final Lock lock = new ReentrantLock();

    public Lock getLock() {
        return lock;
    }

    public Account(String name, int amount) {
        this.name = name;
        this.money = amount;
    }
    
    //加錢
    public void addMoney(int amount){
        money = money + amount;
    }
    //減錢
    public void flyMoney(int amount){
        money = money - amount;
    }

    public String getName() {
        return name;
    }

    public int getAmount() {
        return money;
    }

    @Override
    public String toString() {
        return "Account{" +
                "name='" + name + '\'' +
                ", money=" + money +
                '}';
    }


}
/**
 * 接口
 */
public interface ITransfer {

    void transfer(Account from, Account to, int amount) throws InterruptedException;
}
public class NormalTransfer implements ITransfer{
    @Override
    public void transfer(Account from, Account to, int amount)
            throws InterruptedException {
        synchronized (from){
            System.out.println(Thread.currentThread().getName()+" get "+from.getName());
            Thread.sleep(100);
            synchronized (to){
                System.out.println(Thread.currentThread().getName()
                        +" get "+to.getName());
                from.flyMoney(amount);
                to.addMoney(amount);
            }
        }
    }
}
/**
 * 銀行業務類
 */
public class Bank {

    private static class TransferThread extends Thread{
        private String name;
        private Account from;
        private Account to;
        private int amount;
        private ITransfer transfer;

        public TransferThread(String name, Account from, Account to,
                              int amout,ITransfer transfer) {
            this.name = name;
            this.from = from;
            this.to = to;
            this.amount = amout;
            this.transfer = transfer;
        }

        @Override
        public void run() {
            Thread.currentThread().setName(name);
            try {
                transfer.transfer(from,to,amount);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Bank bank = new Bank();
        Account zhangsan = new Account("zhangsan",20000);
        Account Lisi = new Account("lisi",20000);
        ITransfer transfer = new NormalTransfer();//運行後出現動態順序死鎖

        TransferThread zsToLisi = new TransferThread("zsToLisi",zhangsan,Lisi,
                2000,transfer);
        TransferThread lisiTozs = new TransferThread("lisiTozs",Lisi,zhangsan,
                4000,transfer);
        zsToLisi.start();
        lisiTozs.start();

    }

解決動態順序死鎖緩存

/**
 * 動態順序鎖  比較hash大小
 *  identityHashCode  返回hashcode 無論給定的對象曾經是否複寫過hashcode 
 */
public class SafeTransfer implements ITransfer {

    private static Object tieLock = new Object();

    @Override
    public void transfer(Account from, Account to, int amount)
            throws InterruptedException {

        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);

        if(fromHash<toHash){
            synchronized (from){
                System.out.println(Thread.currentThread().getName()+" get "+from.getName());
                Thread.sleep(100);
                synchronized (to){
                    System.out.println(Thread.currentThread().getName()
                            +" get "+to.getName());
                    from.flyMoney(amount);
                    to.addMoney(amount);
                    System.out.println(from);
                    System.out.println(to);
                }
            }
        }else if(toHash<fromHash){
            synchronized (to){
                System.out.println(Thread.currentThread().getName()+" get "+to.getName());
                Thread.sleep(100);
                synchronized (from){
                    System.out.println(Thread.currentThread().getName()
                            +" get "+from.getName());
                    from.flyMoney(amount);
                    to.addMoney(amount);
                    System.out.println(from);
                    System.out.println(to);
                }
            }
        }else{
            synchronized (tieLock){
                synchronized (to){
                    System.out.println(Thread.currentThread().getName()+" get "+from.getName());
                    Thread.sleep(100);
                    synchronized (from){
                        System.out.println(Thread.currentThread().getName()
                                +" get "+to.getName());
                        from.flyMoney(amount);
                        to.addMoney(amount);
                    }
                }
            }
        }
    }
}
/**
 * 定時輪詢獲取鎖
 */
public class TryLockTransfer implements ITransfer {
    @Override
    public void transfer(Account from, Account to, int amount)
            throws InterruptedException {
        Random r = new Random();
        while(true){
            if(from.getLock().tryLock()){
                try{
                    System.out.println(Thread.currentThread().getName()
                            +" get from "+from.getName());

                    if(to.getLock().tryLock()){
                        try{
                            System.out.println(Thread.currentThread().getName()
                                    +" get to "+to.getName());
                            from.flyMoney(amount);
                            to.addMoney(amount);
                            System.out.println(from);
                            System.out.println(to);
                            break;
                        }finally {
                            to.getLock().unlock();
                        }
                    }

                }finally {
                   from.getLock().unlock();
                }
            }
            Thread.sleep(r.nextInt(5));//防止產生活鎖
        }
    }
}

乾貨:查看線程中棧狀況安全

jdkbin下的jstack.exe

進入bin目錄

執行 jps -m 找到當前程序id號 

再執行 jstack id號 查看

解決死鎖:性能優化

保證正確的加鎖、使用定時鎖多線程

其餘活躍性危險:其餘類型的死鎖(資源爭奪)、飢餓(長久拿不到鎖沒法工做)、糟糕的響應性能(多個應用不一樣業務,其中一個業務佔有運行資源)、活鎖(嘗試獲取時發生碰撞,又釋放又碰撞,能夠經過sleep來減小次數)併發

對性能的思考

  1. 程序的安全性優於性能的提高
  2. 使用多線程會帶來額外的性能開銷,濫用線程,有可能致使得不償失。
  3. 所謂性能,包含多個指標。例如「多快」:服務時間、等待時間、延遲時間;例如「多少」:吞吐量,例如可伸縮性等等。
  4. 性能的各個指標方面,是徹底獨立的,有時候甚至是相互矛盾。
  5. 因此性能的提高是個包括成本在內多方面權衡和妥協的結果。

性能優化的黃金原則:app

首先保證程序正確,而後再提升運行速度(若是有確切的證據代表程序確實慢)。

Amdahl定律

F :程序中的串行部分,是個百分比(100%-1%),

N:cpu的個數

Speedup:指在增長cpu的狀況下,程序的加速比

線程引入的開銷

上下文的切換

內存同步

阻塞

減小鎖的競爭

快進快出,縮小鎖的範圍,將與鎖無關的,有大量計算或者阻塞操做的代碼移出同步範圍。

減少鎖的粒度,多個相互獨立的狀態變量可使用多個鎖來保護,每一個鎖只保護一個變量。

鎖的分段,例如ConcurrentHashMap中的實現。

減小獨佔鎖的使用,例如讀多寫少的狀況下,用讀寫鎖替換排他鎖。

 

安全的單例模式

/**
 * 餓漢式單例
 */
public class SingleEHan {
    public static SingleEHan singleEHan = new SingleEHan();
    private SingleEHan(){};

}
/**
 * 懶漢式單例-雙重檢查(不推薦使用了)  
 */
public class SingleDcl {

    private volatile static SingleDcl single;
    private SingleDcl(){}

    public static SingleDcl getInstance(){
        if(null==single){
            synchronized (SingleDcl.class){
                if(single==null){
                    single = new SingleDcl();
                }
            }
        }
        return single;
    }
}
/**
 * 延遲類佔位符
 */
public class SingleClassInit {
    private String name;
    private SingleClassInit(){}

    private static class InstanceHolder{
        public static SingleClassInit instance = new SingleClassInit();
    }

    public static SingleClassInit getInstance(){
        return InstanceHolder.instance;
    }
}

 

內存模型的抽象結構

物理計算機中的併發問題,物理機遇到的併發問題與虛擬機中的狀況有很多類似之處,物理機對併發的處理方案對於虛擬機的實現也有至關大的參考意義。

「讓計算機併發執行若干個運算任務」與「更充分地利用計算機處理器的效能」之間的因果關係,看起來瓜熟蒂落,實際上它們之間的關係並無想象中的那麼簡單,其中一個重要的複雜性來源是絕大多數的運算任務都不可能只靠處理器「計算」就能完成,處理器至少要與內存交互,如讀取運算數據、存儲運算結果等,這個I/O操做是很難消除的(沒法僅靠寄存器來完成全部運算任務)。因爲計算機的存儲設備與處理器的運算速度有幾個數量級的差距,因此現代計算機系統都不得不加入一層讀寫速度儘量接近處理器運算速度的高速緩存(Cache)來做爲內存與處理器之間的緩衝:將運算須要使用到的數據複製到緩存中,讓運算能快速進行,當運算結束後再從緩存同步回內存之中,這樣處理器就無須等待緩慢的內存讀寫了。

基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,可是也爲計算機系統帶來更高的複雜度,由於它引入了一個新的問題:緩存一致性(Cache Coherence)。在多處理器系統中,每一個處理器都有本身的高速緩存,而它們又共享同一主內存(MainMemory)。當多個處理器的運算任務都涉及同一塊主內存區域時,將可能致使各自的緩存數據不一致,若是真的發生這種狀況,那同步回到主內存時以誰的緩存數據爲準呢?爲了解決一致性的問題,須要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操做。所謂的「內存模型」一詞,能夠理解爲在特定的操做協議下,對特定的內存或高速緩存進行讀寫訪問的過程抽象。

從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(Main Memory)中,每一個線程都有一個私有的本地內存(Local Memory),本地內存中存儲了該線程以讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存、寫緩衝區、寄存器以及其餘的硬件和編譯器優化。

在JMM中若是線程A與線程B之間要通訊的話,必需要經歷下面2個步驟。

1)線程A把本地內存A中更新過的共享變量刷新到主內存中去。

2)線程B到主內存中去讀取線程A以前已更新過的共享變量。

 

volatile的實現原理

volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的「可見性」。

可見性的意思是當一個線程修改一個共享變量時,另一個線程能讀到這個修改的值。若是volatile變量修飾符使用恰當的話,它比synchronized的使用和執行成本更低,由於它不會引發線程上下文的切換和調度。

volatile是如何來保證可見性的呢?是由於在編譯的時候使用了一個Lock前綴的指令,Lock前綴的指令在多核處理器下會引起了兩件事情,

1)將當前處理器緩存行的數據寫回到系統內存。

2)這個寫回內存的操做會使在其餘CPU裏緩存了該內存地址的數據無效。

爲了提升處理速度,處理器不直接和內存進行通訊,而是先將系統內存的數據讀到內部緩存(L1,L2或其餘)後再進行操做,但操做完不知道什麼時候會寫到內存。若是對聲明瞭volatile的變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是,就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算操做就會有問題。因此,在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存裏。

 

synchronized的實現原理

synchronized一直是元老級角色,不少人都會稱呼它爲重量級鎖。可是,隨着Java SE 1.6對synchronized進行了各類優化以後,有些狀況下它就並不那麼重了。

Java中的每個對象均可以做爲鎖。具體表現爲如下3種形式。

·對於普通同步方法,鎖是當前實例對象。

·對於靜態同步方法,鎖是當前類的Class對象。

·對於同步方法塊,鎖是Synchonized括號裏配置的對象。

當一個線程試圖訪問同步代碼塊時,它首先必須獲得鎖,退出或拋出異常時必須釋放鎖。那麼鎖到底存在哪裏呢?鎖裏面會存儲什麼信息呢?

synchronized用的鎖是存在Java對象頭裏的。若是對象是數組類型,則虛擬機用3個字寬(Word)存儲對象頭,若是對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,1字寬等於4字節。

Java SE 1.6爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」,在Java SE 1.6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提升得到鎖和釋放鎖的效率

 

偏向鎖

HotSpot的做者通過研究發現,大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,爲了讓線程得到鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,之後該線程在進入和退出同步塊時不須要進行CAS操做來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖。若是測試成功,表示線程已經得到了鎖。若是測試失敗,則須要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):若是沒有設置,則使用CAS競爭鎖;若是設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標誌位設爲「01」,即偏向模式。同時使用CAS操做把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中,若是CAS操做成功,持有偏向鎖的線程之後每次進入這個鎖相關的同步塊時,虛擬機均可以再也不進行任何同步操做(例如Locking、Unlocking及對Mark Word的Update等)。當有另一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束。根據鎖對象目前是否處於被鎖定的狀態,撤銷偏向(Revoke Bias)後恢復到未鎖定(標誌位爲「01」)或輕量級鎖定(標誌位爲「00」)的狀態

 

輕量級鎖

(1)輕量級鎖加鎖

線程在執行同步塊以前,JVM會先在當前線程的棧楨中建立用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中,官方稱爲Displaced Mark Word。而後線程嘗試使用CAS將對象頭中的Mark Word替換爲指向鎖記錄的指針。若是成功,當前線程得到鎖,若是失敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

(2)輕量級鎖解鎖

輕量級解鎖時,會使用原子的CAS操做將Displaced Mark Word替換回到對象頭,若是成功,則表示沒有競爭發生。若是失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。

輕量級鎖是在無競爭的狀況下使用CAS操做去消除同步使用的互斥量,那偏向鎖就是在無競爭的狀況下把整個同步都消除掉,連CAS操做都不作了。

 

重量級鎖

重量鎖在JVM中又叫對象監視器(Monitor),除了具有Mutex(0|1)互斥的功能,它還負責實現了Semaphore(信號量)的功能,也就是說它至少包含一個競爭鎖的隊列,和一個信號阻塞隊列(wait隊列),前者負責作互斥,後一個用於作線程同步。

 

鎖的優缺點對比

優勢

缺點

適用場景

偏向鎖

加鎖和解鎖不須要額外的消耗,和執行非同步方法比僅存在納秒級的差距

若是線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗

適用於只有一個線程訪問同步塊場景

輕量級鎖

競爭的線程不會阻塞,提升了程序的響應速度

若是始終得不到鎖競爭的線程使用自旋會消耗CPU

追求響應時間,鎖佔用時間很短

重量級鎖

線程競爭不使用自旋,不會消耗CPU

線程阻塞,響應時間緩慢

追求吞吐量,鎖佔用時間較長

 

原子操做的實現原理

使用基於對緩存加鎖或總線加鎖的方式來實現多處理器之間的原子操做。首先處理器會自動保證基本的內存操做的原子性。處理器保證從系統內存中讀取或者寫入一個字節是原子的,意思是當一個處理器讀取一個字節時,其餘處理器不能訪問這個字節的內存地址。可是複雜的內存操做處理器是不能自動保證其原子性的。處理器提供總線鎖定和緩存鎖定兩個機制來保證複雜內存操做的原子性。

(1)使用總線鎖保證原子性

第一個機制是經過總線鎖保證原子性。若是多個處理器同時對共享變量進行讀改寫操做(i++就是經典的讀改寫操做),那麼共享變量就會被多個處理器同時進行操做,這樣讀改寫操做就不是原子的,操做完以後共享變量的值會和指望的不一致。處理器使用總線鎖就是來解決這個問題的。所謂總線鎖就是使用處理器提供的一個LOCK#信號,當一個處理器在總線上輸出此信號時,其餘處理器的請求將被阻塞住,那麼該處理器能夠獨佔共享內存。

(2)使用緩存鎖保證原子性

在同一時刻,咱們只需保證對某個內存地址的操做是原子性便可,但總線鎖定把CPU和內存之間的通訊鎖住了,這使得鎖按期間,其餘處理器不能操做其餘內存地址的數據,因此總線鎖定的開銷比較大,目前處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。緩存鎖定就是當某塊CPU對緩存中的數據進行操做了以後,就通知其餘CPU放棄儲存在它們內部的緩存,或者從主內存中從新讀取。

處理器提供了不少Lock前綴的指令來實現。例如,位測試和修改指令:BTS、BTR、BTC;交換指令XADD、CMPXCHG。JVM中的CAS操做正是利用了處理器提供的CMPXCHG指令實現的。Java中能夠經過鎖和循環CAS的方式來實現原子操做。

重排序

在執行程序時,爲了提升性能,編譯器和處理器經常會對指令作重排序。重排序分3種類型。

1)編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,能夠從新安排語句的執行順序。

2)指令級並行的重排序。現代處理器採用了指令級並行技術來將多條指令重疊執行。若是不存在數據依賴性,處理器能夠改變語句對應機器指令的執行順序。

3)內存系統的重排序。因爲處理器使用緩存和讀/寫緩衝區,這使得加載和存儲操做看上去多是在亂序執行。

無論怎麼重排序(編譯器和處理器爲了提升並行度),程序的執行結果不能被改變。編譯器、runtime和處理器都必須遵照as-if-serial語義。爲了遵照as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操做作重排序,由於這種重排序會改變執行結果。可是,若是操做之間不存在數據依賴關係,這些操做就可能被編譯器和處理器重排序。

例如:

double pi = 3.14; // A

double r = 1.0; // B

double area = pi * r * r; // C

A和C之間存在數據依賴關係,同時B和C之間也存在數據依賴關係。所以在最終執行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的結果將會被改變)。但A和B之間沒有數據依賴關係,編譯器和處理器能夠重排序A和B之間的執行順序。

 

happens-before

爲了保證內存可見性,Java編譯器在生成指令序列的適當位置會插入內存屏障指令來禁止特定類型的處理器重排序,爲此jvm中提出了happens-before的概念來指定兩個操做之間的執行順序。因爲這兩個操做能夠在一個線程以內,也能夠是在不一樣線程之間。JMM能夠經過happens-before關係向程序員提供跨線程的內存可見性保證(若是A線程的寫操做a與B線程的讀操做b之間存在happensbefore關係,儘管a操做和b操做在不一樣的線程中執行,但JMM向程序員保證a操做將對b操做可見)。

happens-before關係的定義以下。

1)若是一個操做happens-before另外一個操做,那麼第一個操做的執行結果將對第二個操做可見。

2)兩個操做之間存在happens-before關係,並不意味着Java平臺的具體實現必需要按照happens-before關係指定的順序來執行。若是重排序以後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不非法(也就是說,JMM容許這種重排序)。

兩個操做之間具備happens-before關係,並不意味着前一個操做必需要在後一個操做以前執行!happens-before僅僅要求前一個操做(執行的結果)對後一個操做可見。

上面計算圓的面積的示例代碼存在3個happens-before關係,以下。

·A happens-before B。

·B happens-before C。

·A happens-before C。

在3個happens-before關係中,2和3是必需的,但1是沒必要要的。

Java內存模型下一些先行發生關係。若是兩個操做之間的關係不在此列,而且沒法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機能夠對它們隨意地進行重排序。

程序次序規則(Program Order Rule:在一個線程執行一個方法時,按照程序代碼順序,書寫在前面的操做先行發生於書寫在後面的操做。準確地說,應該是控制流順序而不是程序代碼順序,由於要考慮分支、循環等結構。

管程鎖定規則(Monitor Lock Rule):一個unlock操做先行發生於後面對同一個鎖的lock操做。這裏必須強調的是同一個鎖,而「後面」是指時間上的前後順序。

volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做,這裏的「後面」一樣是指時間上的前後順序。

線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每個動做。

線程終止規則(Thread Termination Rule):線程中的全部操做都先行發生於對此線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。

線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測到是否有中斷髮生。

對象終結規則(Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法的開始。

傳遞性(Transitivity):若是操做A先行發生於操做B,操做B先行發生於操做C,那就能夠得出操做A先行發生於操做C的結論。

例如代碼:

顯示的是一組再普通不過的getter/setter方法,假設存在線程A和B,線程A先(時間上的前後)調用了「setValue(1)」,而後線程B調用了同一個對象的「getValue()」,那麼線程B收到的返回值是什麼?

咱們依次分析一下先行發生原則中的各項規則,因爲兩個方法分別由線程A和線程B各自run方法中調用,不在一個線程中,因此程序次序規則在這裏不適用;因爲沒有同步塊,天然就不會發生lock和unlock操做,因此管程鎖定規則不適用;因爲value變量沒有被volatile關鍵字修飾,因此volatile變量規則不適用;後面的線程啓動、終止、中斷規則和對象終結規則也和這裏徹底沒有關係。由於沒有一個適用的先行發生規則,因此最後一條傳遞性也無從談起,所以咱們能夠斷定儘管線程A在操做時間上先於線程B,可是沒法肯定線程B中「getValue()」方法的返回結果,換句話說,這裏面的操做不是線程安全的。

那怎麼修復這個問題呢?咱們至少有兩種比較簡單的方案能夠選擇:要麼把getter/setter方法都定義爲synchronized方法,這樣就能夠套用管程鎖定規則;要麼把value定義爲volatile變量,這樣就能夠套用volatile變量規則來實現先行發生關係。

 

volatile的內存語義

理解volatile特性的一個好方法是把對volatile變量的單個讀/寫,當作是使用同一個鎖對這些單個讀/寫操做作了同步。

假設有多個線程分別調用上面程序的3個方法,這個程序在語義上和下面程序等價。

volatile寫的內存語義以下。

當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量值刷新到主內存。

volatile讀的內存語義以下。

當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

 

鎖的內存語義

鎖是Java併發編程中最重要的同步機制。鎖除了讓臨界區互斥執行外,還可讓釋放鎖的線程向獲取同一個鎖的線程發送消息。

當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。

當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量。

相關文章
相關標籤/搜索