Java多線程基礎(二)——Java內存模型

1、主存儲器與工做存儲器

Java內存模型(memory model)分爲主存儲器(main memory)和工做存儲器(working memory)兩種。設計模式

主存儲器(main memory):
類的實例所存在的區域,main memory爲全部的線程所共享。安全

工做存儲器(working memory):
每一個線程各自獨立所擁有的做業區,在working memory中,存有main memory中的部分拷貝,稱之爲工做拷貝(working copy)。性能

1-1 Java內存模型概念圖

2、字段的使用

2.1 字段的引用

線程沒法直接對主存儲器進行操做,當線程須要引用實例的字段的值時,會一次將字段值從主存儲器拷貝到工做存儲器上(至關於上圖中的read->load)。
當線程再次須要引用相同的字段時,可能直接使用剛纔的工做拷貝(use),也可能從新從主存儲器獲取(read->load->use)。
具體會出現哪一種狀況,由JVM決定。atom

2.2 字段的賦值

因爲線程沒法直接對主存儲器進行操做,因此也就沒法直接將值指定給字段。
當線程欲將值指定給字段時,會一次將值指定給位於工做存儲器上的工做拷貝(assign),指定完成後,工做拷貝的內容便會複製到主存儲器(store->write),至於什麼時候進行復制,由JVM決定。
所以,當線程反覆對一個實例的字段進行賦值時,可能只會對工做拷貝進行指定(assign),此時只有指定的最後結果會在某個時刻拷貝到主存儲器(store-write);也可能在每次指定時,都進行拷貝到主存儲器的操做(assign->store->write)。spa

3、線程的原子操做

Java語言規範定義了線程的六種原子操做:線程

  • read
    負責從主存儲器(main memory)拷貝到工做存儲器(working memory)
  • write
    與上述相反,負責從工做存儲器(working memory)拷貝到主存儲器(main memory)
  • use
    表示線程引用工做存儲器(working memory)的值
  • assign
    表示線程將值指定給工做存儲器(working memory)
  • lock
    表示線程取得鎖定
  • unlock
    表示線程解除鎖定

4、synchronied的本質

4.1 線程欲進入synchronized

線程欲進入synchronized時,會執行如下兩類操做:設計

  • 強制寫入主存儲器(main memory)

當線程欲進入synchronized時,若是該線程的工做存儲器(working memory)上有未映像到主存儲器的拷貝,則這些內容會強制寫入主存儲器(store->write),則這些計算結果就會對其它線程可見(visible)。code

  • 工做存儲器(working memory)的釋放

當線程欲進入synchronized時,工做存儲器上的工做拷貝會被所有丟棄。以後,欲引用主存儲器上的值的線程,一定會從主存儲器將值拷貝到工做拷貝(read->load)。內存

4.2 線程欲退出synchronized

線程欲退出synchronized時,會執行如下操做:rem

  • 強制寫入主存儲器(main memory)

當線程欲退出synchronized時,若是該線程的工做存儲器(working memory)上有未映像到主存儲器的拷貝,則這些內容會強制寫入主存儲器(store->write),則這些計算結果就會對其它線程可見(visible)。

注意: 線程欲退出synchronized時,不會執行工做存儲器(working memory)的釋放 操做。

5、volatile的本質

volatile具備如下兩種功能:

  • 進行內存同步

volatile只能作內存同步,不能取代synchronized關鍵字作線程同步。
當線程欲引用volatile字段的值時,一般都會發生從主存儲器到工做存儲器的拷貝操做;相反的,將值指定給寫着volatile的字段後,工做存儲器的內容一般會當即映像到主存儲器

  • 以原子(atomic)方式進行long、double的指定

6、Double Checked Locking Pattern的危險性

6.1 可能存在缺陷的單例模式

設計模式中有一種單例模式(Singleton Pattern),一般採用鎖來保證線程的安全性。

Main類:

//兩個Main線程同時調用單例方法getInstance
public class Main extends Thread {
    public static void main(String[] args) {
        new Main().start();
        new Main().start();
    }
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":" + MySystem.getInstance().getDate());
    }
}

單例類:

//採用延遲加載+雙重鎖的形式保證線程安全以及性能
public class MySystem {
    private static MySystem instance = null;
    private Date date = new Date();
 
    private MySystem() {
    }
    public Date getDate() {
        return date;
    }
    public static MySystem getInstance() {
        if (instance == null) {
            synchronized (MySystem.class) {
                if (instance == null) {
                    instance = new MySystem();
                }
            }
        }
        return instance;
    }
}

分析:
上述Main類的MySystem.getInstance().getDate()調用可能返回null或其它值。
假設有兩個線程A和B,按照如下順序執行:

當線程A執行完A-4且未退出synchronized時,線程B開始執行,此時B得到了A建立好的instance實例。
可是注意,此時instance實例可能並未徹底初始化完成。
這是由於線程A製做MySystem實例時,會給date字段指定值new Date(),此時可能只完成了assign操做(線程A對工做存取器上的工做拷貝進行指定),在線程A退出synchronized時,線程A的工做存儲器上的值不保證必定會映像到主存儲器上(store->write)。

因此,當線程B在線程A退出前就調用MySystem.getInstance().getDate()方法的話,因爲主存儲器上的date字段並未被賦值過,因此B獲得的date字段就是未初始化過的。

注意:上面描述的這種狀況是否真的會發生,取決於JVM,由Java語言規範決定。

解決方法:
採用懶加載模式,在MySystem類中直接爲instance 字段賦值:
private static MySystem instance = new MySystem();

相關文章
相關標籤/搜索