天天看點

java并發程式設計一一多線程線程安全(三)

1.多線程的三大特性

1.1什麼是原子性

即一個操作或多個操作要麼全部執行并且執行的過程不會被任何因素打斷,要麼就都不執行。

一個很經典的例子就是銀行賬戶轉賬問題:

比如從賬戶A向賬戶B轉1000元,那麼必然包括2個操作:從賬戶A減去1000元,往賬戶B加上1000元。這2個操作必須要具備原子性才能保證不出現一些意外的問題。

我們操作資料也是如此,比如i = i+1;其中就包括,讀取i的值,計算i,寫入i。這行代碼在Java中是不具備原子性的,則多線程運作肯定會出問題,是以也需要我們使用同步和lock這些東西來確定這個特性了。

原子性其實就是保證資料一緻、線程安全一部分,

1.2什麼是可見性

當多個線程通路一個變量時,一個線程修改了這個變量的值,其它線程能夠立即看到得到這個修改的值。

若兩個線程在不同的cpu,那麼線程1 改表 i 的值還沒有重新整理到主存,線程2又使用 i 的值,

那麼這個i 的值肯定還是之前的,線程1 變量的修改沒有看到就是可見性問題。

1.3什麼是有序性

程式的執行順序按照代碼的先後順序執行。

一般來說處理器為了提高程式的運作效率,可能就會對輸入的代碼進行優化,他不保證程式中各個語句

的執行先後順序同代碼中的順序一緻,但是他會保證程式最終執行結果和代碼順序執行的

結果一緻的。

示例如下:

int a = 10; //語句1

int r = 2; // 語句2

a = a + 3; // 語句3

r = a* a; // 語句4

則因為重排序,它還可能執行的順序是:2-1-3-4,1-3-2-4

但是絕對不可能是 2-1-4-3 ,因為這個打破了依賴關系。

顯然重排序對單線程運作是不會有任何問題的,但是對于多線程就不一定了,

是以我們在多線程程式設計時就得考慮這個問題。

2.Java記憶體模型

2.1什麼是java記憶體模型

共享記憶體模型指的就是 java 記憶體模型(簡稱 JMM)jmm決定了一個線程對共享變量的寫入時,

能對另一個線程可見。從抽象的角度來看,jmm 定義了線程和主存之間的抽象關系:線程之間的

共享變量儲存在主存(main memory)中,每個線程都有一個私有的本地記憶體(local memory),

本地記憶體中儲存了該線程已讀/寫共享變量的副本。本地記憶體是jmm的一個抽象概念,并不真實存在。

它涵蓋了緩存,寫緩存區,寄存器以及其他的硬體和編譯器優化。

java并發程式設計一一多線程線程安全(三)

上圖分析:

線程A 與線程B 之間如要通信的話,必須要經曆下面2個步驟:

1. 首先,線程A把本地記憶體A 中更新過的共享變量重新整理到主記憶體中。

2. 然後,線程B到主記憶體中去讀線程A之前已更新過的共享變量

下面通過示意圖來說明這兩個步驟:

java并發程式設計一一多線程線程安全(三)

如上圖是以:本地記憶體A和B有主記憶體中共享變量x的副本。假設初始時,這三個記憶體中的x值都為0。

線程A在執行時,把更新後的x值(假設值為1)臨時存放在自己的本地記憶體中A 中。當線程A 和線程B

需要通信時,線程A 首先會把自己本地記憶體中修改的x刷到主記憶體中,此時主記憶體中x值變了1.随後,

線程B 到主記憶體中去讀線程A更新後的X值,此時線程B的本地記憶體的x值也變了1.

從整體來看,這兩個步驟實質是線程A 在向線程B 發消息,而且這個通信過程必須要經過主記憶體。

JMM通過控制主記憶體與每個線程的本地記憶體之間的互動,來為java程式員提供記憶體可見性保證。

總結:

什麼是java記憶體模型?

java 記憶體模型 簡稱 jmm ,定義一個線程對另一個線程可見。共享變量存放在主記憶體中,每個線程

都有自己的本地記憶體,當多個線程同時通路一個資料的時候,可能本地記憶體沒有及時重新整理到主存中,

所有就會發生線程安全問題。

3.Volatile

3.1什麼是Volatile

可見性也就是一旦某個線程修改了該被volatile修飾的變量,他也保證修改的值會立即被更新到主記憶體,

當有其它線程需要讀取時,可以立即擷取修改之後的值

在java中為了加快程式的運作效率,對一些變量的操作通常是在該線程的寄存器或是CPU緩存

上進行的,之後才會同步到主記憶體中,而加了volatile修飾符的變量則是直接讀寫到竹村中。

Volatile保證了線程間共享變量的及時可見性,但不能保證原子性。

代碼示例:

class ThreadVolatileDemo extends Thread {
    public    boolean flag = true;
    @Override
    public void run() {
        System.out.println("開始執行子線程....");
        while (flag) {
        }
        System.out.println("線程停止");
    }
    public void setRuning(boolean flag) {
        this.flag = flag;
    }
}

public class ThreadVolatile {
    public static void main(String[] args) throws InterruptedException {
        ThreadVolatileDemo threadVolatileDemo = new ThreadVolatileDemo();
        threadVolatileDemo.start();
        Thread.sleep();
        threadVolatileDemo.setRuning(false);
        System.out.println("flag 已經設定成false");
        Thread.sleep();
        System.out.println(threadVolatileDemo.flag);
    }
}
           

已經将結構設定為false為什麼還一直運作呢?

原因:線程之間是不可見的,讀取的是副本,沒有及時讀取到主記憶體的結構。

解決方法是使用volatile關鍵字将解決線程之間可以見性,強制線程每次讀取該值的時候

都去“主記憶體”中取值。

3.2Volatile的特性

  1. 保證此變量對所有的線程的可見性,這裡的“可見性”。

    如文本開頭所述,當一個線程修改了這個變量的值,volatile保證了新值能立即同步到主存中,

    以及每次使用前立即從主存中重新整理。但是普通的變量做不到這點,普通變量的值線上程間傳遞均需要通過主存來完成。

  2. 禁止指令重排序優化。

    有volatile修飾的變量,指派後多執行一個“load addl $0x0,(%esp)” 操作,這個操作相當于一個

    記憶體屏障(指令重排序時不能把後面的指令重排序到記憶體屏障之前的位置),隻有一個CPU

    通路記憶體時,并不需要記憶體屏障;(什麼是指令重排序:是指CPU采用了允許建多條指令不安程式的

    的順序分開發送給相應電路單元處理。)

  3. volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因為他需要在本地代碼中

    插入許多記憶體屏障指令來保證處理器不發生亂序執行。

3.3Volatile與synchronized的差別

  1. 進而我們可以看出volatile 雖然具有可見性但是并不能保證原子性。
  2. 性能方面,synchronized關鍵字時防止多個線程同時之sing一段代碼,就會影響程式執行效率,

    而volatile 關鍵字在某些情況下性能要優于synchronized。

    但是要注意的volatile 關鍵字時無法替代synchrond 關鍵字的,因為volatile關鍵字無法保證原子性。