1. 程式人生 > >[Java並發編程實戰] 共享對象之可見性

[Java並發編程實戰] 共享對象之可見性

Java 可見性 java並發編程 同步鎖 Volatile變量

盛年不重來,一日難再晨,及時當勉勵,歲月不待人。」  陶淵明

我們已經知道同步代碼塊和同步方法可以保證以原子的方式執行,其實,同步還有另外一個重要概念:內存可見性。換句話說,我們不僅希望防止某個線程正在使用對象狀態而另一個線程同時在修改狀態,而且希望確保當一個線程修改了對象的狀態後,其他線程能夠看到修改後的狀態。

可見性

一個線程對共享變量值的修改,能夠及時的被其他線程看到。可見性微妙的,這是因為可能發生錯誤的事情總是與直覺大相徑庭。來看下面這個例子和他的執行結果:

 1public class NoVisibility {
2 private static boolean ready;
3
private static int number;
4 private static class ReaderThread extends Thread {
5 public void run() {
6 while(!ready)
7 Thread.yield();
8 System.out.println(number);
9 }
10 }
11 public static void main(String[] args) {
12 // TODO Auto-generated method stub

13 new ReaderThread().start();
14 number = 88;
15 ready = true;
16 }
17}

上面的代碼清單,親測執行的結果是88。
然而,書本上的解釋是可能出現錯誤的結果。錯誤的結果有下面兩種情況(我重現不到下面的結果):

  1. NoVisibility 可能會一直保持循環,因為對讀線程來說,主線程寫給 ready 的值可能永遠對讀線程不可見。

  2. NoVisibility 可能會打印0,因為早在對 number 賦值之前,主線程就已經寫入 ready 並使之對讀線程可見,這是一種重排序。

即可親測沒有發生,但是可能會發生。為了防止這種現象的發生,只能通過對共享變量進行恰當的同步。

Java 內存模型(JMM,Java Memory Model)

描述了 java 程序中各種變量(線程共享變量)的訪問規則,以及在 JVM 中將變量存儲到內存和從內存中讀取出變量的底層細節。

技術分享圖片這裏寫圖片描述

所有變量都存儲在主內存中,每個線程都有自己獨立的工作內存,裏面保存該線程使用到的變量副本,即主內存中該變量的一份拷貝。

線程對共享變量的所有操作必須在自己的工作內存,線程間變量值的傳遞需要通過主內存來完成。

加鎖與可見性

加鎖的含義不僅僅局限於互斥行為,還包括內存可見性。為了確保所有線程都能看到共享變量的最新值,所有執行讀操作或者寫操作都必須在同一個鎖上同步。

技術分享圖片這裏寫圖片描述

當線程 B 執行有鎖保護的代碼塊時,可以看到線程 A 之前在同一個同步代碼塊中所有的操作結果。這就是為啥要求所有線程在同一個鎖上同步,為了確保某個線程寫入該變量的值對於其他線程來說是可見的。

非原子的64位操作

JVM 允許將64位的讀操作或寫操作分解為兩個32位的操作。Java 中的 long 類型和 double 類型是64位的,所以當讀取一個非 volatile 類型的 long 變量時,如果該變量的讀操作和寫操作在不同的線程中執行,那麽很可能會讀取到某個值的高32位和另一個值的低32位。因此,在多線程中使用共享的可變的 long 和 double 類型變量時不安全的,除非用關鍵字 volatile 來聲明他們,或者用鎖保護起來。

volatile變量

Java 提供了一種稍弱的同步機制,即 volatile 變量,用來確保將變量的更新操作通知到其他線程。volatile 變量具有 synchronized 的可見性,但是不具備原子特性。要使 volatile 變量提供理想的線程安全,必須同時滿足下面兩個條件:

  • 對變量的寫操作不依賴於自身當前值

  • 該變量沒有包含在具有其他變量的不變式中

volatile 通常被當做標識完成、中斷、狀態的標記使用。典型應用如下代碼,檢查狀態標記,以確定是否退出一個循環。

1volatile boolean asleep;
2 while(!asleep)
3 countSomeSheep();

當然,上面也可以用鎖,但是會讓代碼變得復雜。volatile 變量不會加鎖,也就不會引起線程的阻塞,相比 sychronized, 這只是輕量級的同步機制。盡管 volatile 也可以用來標識其他類型的狀態信息,但是要格外小心。比如, volatile 的語義不足以使自增操作(count++)原子化。

本文原創首發於微信公眾號 [ 林裏少年 ],歡迎關註獲取更新。

技術分享圖片


[Java並發編程實戰] 共享對象之可見性