如何使用 volatile, synchronized, final 進行線程間通訊

原文地址:https://segmentfault.com/a/1190000004487149。感謝做者的無私分享。java

你是否真正理解並會用volatile, synchronized, final進行線程間通訊呢,若是你不能回答下面的幾個問題,那就說明你並無真正的理解:nginx

  1. volatile變量的操做必定具備原子性嗎?segmentfault

  2. synchronized所謂的加鎖,鎖住的是什麼?數組

  3. final定義的變量不變的究竟是什麼?緩存

java內存模型

內存模型

看java內存模型以前,咱們先來看看什麼是內存模型?併發

在多處理器系統中,處理器一般有多級緩存,由於這些緩存離處理器更近而且能夠存儲一部分數據,因此緩存能夠改善處理器獲取數據的速度和減小對共享內存數據總線的佔用。緩存雖然能極大的提升性能,可是同時也帶來了諸多挑戰。例如,當兩個處理器同時操做同一個內存地址的時候,該如何處理?這兩個處理器在什麼條件下才能看到相同的值?性能

對於處理器而言,一個內存模型就是定義一些充分必要的規範,這些規範使得其餘處理器對內存的寫操做對當前處理器可見,或者當前處理器的寫操做對其餘處理器可見。學習

其餘處理器對內存的寫必定發生在當前處理器對同一內存的讀以前,稱之爲其餘處理器對內存的寫對當前處理器可見。

Java 內存模型

知道了內存模型,那麼應該能夠更好的理解java內存模型。spa

簡單的講,java內存模型指的就是一套規範,如今最新的規範爲JSR-133。這套規範包含:線程

  1. 線程之間如何經過內存通訊

  2. 線程之間經過什麼方式通訊才合法,才能獲得指望的結果

Java 內存模型中的內存結構

咱們已經知道 java 內存模型就是一套規範,那麼在這套規範中,規定的內存結構是什麼樣的呢?

簡單的講,Java 內存模型將內存分爲共享內存和本地內存。共享內存又稱爲堆內存,指的就是線程之間共享的內存,包含全部的實例域、靜態域和數組元素。每一個線程都有一個私有的,只對本身可見的內存,稱之爲本地內存。

java內存模型中的內存結構以下圖所示:

共享內存中共享變量雖然由全部的線程共享,可是爲了提升效率,線程並不直接使用這些變量,每一個線程都會在本身的本地內存中存儲一個共享內存的副本,使用這個副本參與運算。因爲這個副本的參與,致使了線程之間對共享內存的讀寫存在可見性問題。

爲了方便線程之間的通訊,java 提供了 volatile, synchronized, final 三個關鍵字供咱們使用,下面咱們來看看如何使用它們進行線程間通訊

volatile

volatile 定義的變量,特殊性在於:

一個線程對 volatile 變量的寫必定對以後對這個變量的讀的線程可見。

等價於

一個線程對 volatile 變量的讀必定能看見在它以前最後一個線程對這個變量的寫。

爲了實現這些語義,Java 規定,(1)當一個線程要使用共享內存中的 volatile 變量時,如圖中的變量a,它會直接從主內存中讀取,而不使用本身本地內存中的副本。(2)當一個線程對一個 volatile 變量進行寫時,它會將這個共享變量的值刷新到共享內存中。

咱們能夠看到,其實 volatile 變量保證的是一個線程對它的寫會當即刷新到主內存中,並置其它線程的副本爲無效,它並不保證對 volatile 變量的操做都是具備原子性的。

因爲

public void add(){
     a++;         #1
 }

等價於

public void add() {
    temp = a;        
    temp = temp +1;  
    a = temp;         
 }

代碼1並非一個原子操做,因此相似於 a++ 這樣的操做會致使併發數據問題。

volatile 變量的寫能夠被以後其餘線程的讀看到,所以咱們能夠利用它進行線程間的通訊。如

volatile int a;

public void set(int b) {
    a = b; 
}

public void get() {
    int i = a; 
}

線程A執行set()後,線程B執行get(),至關於線程A向線程B發送了消息。

synchronized

若是咱們非要使用 a++ 這種複合操做進行線程間通訊呢?java 爲咱們提供了synchronized。

public synchronized void add() {
    a++; 
 }

synchronized 使得

它做用範圍內的代碼對於不一樣線程是互斥的,而且線程在釋放鎖的時候會將共享變量的值刷新到共享內存中。

咱們能夠利用這種互斥性來進行線程間通訊。看下面的代碼,

public synchronized void add() {
    a++; 
}

public synchronized void get() {
    int i = a; 
}

當線程A執行 add(),線程B調用get(),因爲互斥性,線程A執行完add()後,線程B才能開始執行get(),而且線程A執行完add(),釋放鎖的時候,會將a的值刷新到共享內存中。所以線程B拿到的a的值是線程A更新以後的。

volatile 和 synchronized比較

根據以上的分析,咱們能夠發現volatile和synchronized有些類似。

  1. 當線程對 volatile變量寫時,java 會把值刷新到共享內存中;而對於synchronized,指的是當線程釋放鎖的時候,會將共享變量的值刷新到主內存中。

  2. 線程讀取volatile變量時,會將本地內存中的共享變量置爲無效;對於synchronized來講,當線程獲取鎖時,會將當前線程本地內存中的共享變量置爲無效。

  3. synchronized 擴大了可見影響的範圍,擴大到了synchronized做用的代碼塊。

final 變量

final關鍵字能夠做用於變量、方法和類,咱們這裏只看final 變量。

final變量的特殊之處在於,

final 變量一經初始化,就不能改變其值。

這裏的值對於一個對象或者數組來講指的是這個對象或者數組的引用地址。所以,一個線程定義了一個final變量以後,其餘任意線程都拿到這個變量。但有一點須要注意的是,當這個final變量爲對象或者數組時,

  1. 雖然咱們不能講這個變量賦值爲其餘對象或者數組,可是咱們能夠改變對象的域或者數組中的元素。

  2. 線程對這個對象變量的域或者數據的元素的改變不具備線程可見性。

總結

有時候,咱們在學習一門技術過程當中,並不能僅僅侷限於怎麼用,知道怎麼用以後,咱們應該深刻的探究一下,爲何這麼用以後就能獲得咱們想要的結果呢?既要知其然,更要知其因此然。

相關文章
相關標籤/搜索