淺談Java併發編程-1-Java內存模型(JMM)

目錄

 

一、可見性簡介

二、synchronized實現可見性原理

三、volatile保證可見性

四、volatile適用場景

五、synchronized和volatile比較

六、總結


 

 

一、可見性簡介

1、可見性:一個線程對共享變量值的修改,能夠及時地被其他線程看到。

2、共享變量:如果一個變量在多個線程的工作內存中都存有副本,那麼這個變量就是共享變量。

3、java內存模型(JMM)

所有的變量都存在主內存中,每個線程都有自己的工作內存,裏面保存着該線程使用到的變量副本(就是主內存中該變量的一份拷貝)。

 

Java內存模型有兩條規定:

1、線程對共享變量的所有操作都必須在自己的工作內存中進行,不能直接對主內存中進行讀寫

2、不同線程之間無法訪問其他線程工作內存中的變量,線程間變量的值傳遞都需要通過主內存來完成。

 

二、synchronized實現可見性原理

synchronized可以實現互斥鎖,可以保證在同一個時刻都只有一個線程在執行鎖裏面的代碼。

synchronized能夠實現:原子性(同步)、可見性

(一)JMM關於synchronized的兩條規定

1、線程解鎖前,必須把共享變量的最新值刷新到主內存中

2、線程加鎖時,將情況工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新讀取最新的值。

 

(二)線程執行互斥代碼的過程:

  1. 獲得互斥鎖

  2. 清空工作內存

  3. 從主內存拷貝變量的最新副本到線程工作內存

  4. 執行代碼

  5. 將更改後的最新的共享變量的值刷新到主內存

  6. 釋放互斥鎖

 

(重排序:代碼書寫的順序與實際執行的順序不同,指令重排序是編譯器或處理器爲了提高程序性而做的優化)

as-if-serial:無論如何重排序,程序執行的結果都應該和代碼順序執行的結果一致。

試着用synchronized關鍵字寫一個讀寫的demo: 

package com.jqs.sync;

public class SynchronizedDemo {

    //共享變量
    private int result = 0;
    private boolean ready = false;
    private int num = 1;

    //寫操作
    public synchronized void write() { //synchronized在此可以保證共享變量的內存可見性,從而線程安全
        ready = true;
        num = 2;

    }

    //讀操作
    public synchronized void read() { //synchronized在此可以保證共享變量的內存可見性,從而線程安全
        if (ready)
            result = num * 3;
        System.out.println("result中的結果爲:" + result);

    }

    private class ReadWriteThread extends Thread {
        private boolean flag;

        public ReadWriteThread(boolean flag) {
            this.flag = flag;
        }

        @Override
        public void run() {
            if (flag)
                write();
            else
                read();
        }


    }

    public static void main(String args[]) {


        SynchronizedDemo demo = new SynchronizedDemo();
        demo.new ReadWriteThread(true).start();
        try {
            Thread.sleep(1000);
            /*
             * synchronized可以保證線程之間不會交叉執行,但是仍有可能因爲讀線程先執行run方法導致結果爲0。(即是寫線程不是一定就會在讀線程執行之前先執行完)
             * 所以在寫線程啓動之後加入主線程的休眠語句,這樣寫線程就可以基本在讀線程執行之前執行完!!!正確結果應該是6
             * */
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        demo.new ReadWriteThread(false).start();


    }

}

注:當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將會被阻塞

 

三、volatile保證可見性

volatile關鍵字:能夠保證volatile變量的可見性,不能保證volatile變量複合操作的原子性

如何實現內存可見性:通過加入內存屏障和禁止重排序優化來實現

簡單的說就是,volatile變量在每次被線程訪問時,都強迫從主內存中讀取該變量的值,而當該變量發生變化時,又會強迫線程將最新的值刷新到主內存,這樣任何時刻,不同的線程總能看到該變量的最新值。

注意:volatile不能保證volatile變量複合操作的原子性

(原子性: 原子性就是指該操作是不可再分的。不論是多核還是單核,具有原子性的量,同一時刻只能有一個線程來對它進行操作。簡而言之,在整個操作過程中不會被線程調度器中斷的操作,都可認爲是原子性。比如 a = 1;):

private volatile int number=1;

number++; 
//不是原子操作

//分爲三步:1、讀number值  2、number值加1 3、寫入number的值

試着寫一個demo

package com.jqs.sync;

public class VolatileDemo {
    private volatile int number = 0;

    public int getNumber() {
        return this.number;
    }

    public void increase() {

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.number++;
    }

    public static void main(String args[]) {
        final VolatileDemo demo = new VolatileDemo();
        for (int i = 0; i < 500; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    demo.increase();
                }
            }).start();
        }


        /*
         * 如果還有子線程還在執行,主線程就讓出cpu資源
         * 直到所有的子線程都運行完了,主線程再往下執行
         * */
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }

        System.out.println("number:" + demo.getNumber());
    }
    //由於number++不是原子操作,對應的三步操作有可能會有線程交叉執行,最終導致結果可能不爲500

}

存在這種情況的原因,就是線程A讀取到了num值,但是執行時被B佔用了cup,num被B加一併且寫入了主內存,但是此時A線程中的num值還沒有加一,之後A再加一將值寫入,就不對了。

想要解決的話,就把number++替換爲

synchronized (this){

this.number++;

}

或者用到lock,具體看代碼


Lock lock=new ReentrantLock();

lock.lock();

try {

this.number++;

}finally {

lock.unlock();

}

 

四、volatile適用場景

1、對變量的寫入操作不依賴其當前值

不滿足的:number++、count=count*5

滿足的:boolean變量、溫度記錄變量等

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

如 low<up

 

五、synchronized和volatile比較

1、volatile不需要加鎖,比synchronized更輕量級,不會阻塞線程

2、從內存可見性的角度講,volatile讀相當於加鎖,volatile寫相當於解鎖

3、synchronized既能保證可見性,又可保證原子性,而volatile只能保證可見性,不能保證原子性。總結來說,就是synchronized可以保證線程安全,但是volatile沒辦法保證線程安全

六、總結

如果既想要線程安全又想性能好的話,synchronized和volatile要結合具體情況使用。

synchronized並不是萬能的,在加鎖時能夠鎖局部就不要鎖住整體,能夠鎖變量就不要鎖方法。在保證線程安全的前提下,加鎖的範圍越小越好。

 

參考:《Java併發編程實踐》

 

如有錯誤,歡迎指出,以上。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章