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的一個抽象概念,並不真實存在。
它涵蓋了緩存,寫緩存區,寄存器以及其他的硬件和編譯器優化。
這裏寫圖片描述
上圖分析:
線程A 與線程B 之間如要通信的話,必須要經歷下面2個步驟:
1. 首先,線程A把本地內存A 中更新過的共享變量刷新到主內存中。
2. 然後,線程B到主內存中去讀線程A之前已更新過的共享變量
下面通過示意圖來說明這兩個步驟:
這裏寫圖片描述
如上圖所以:本地內存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(3000);
        threadVolatileDemo.setRuning(false);
        System.out.println("flag 已經設置成false");
        Thread.sleep(1000);
        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關鍵字無法保證原子性。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章