你須要瞭解鎖的前提-volatile

前言

java 中鼎鼎有名的 AQS 維護 private volatile int state 狀態實現了用戶態的鎖。你若是不瞭解 volatile ,你看 AQS 的源碼應該很難理解爲何Lock 能保證線程安全。java

volatile 絕對是你打通 java 的任督二脈的首要條件。votaile 的特性很簡單,可見性禁止指令重拍,若是你本身寫代碼驗證過這兩個特色,接下來的內容應該對你幫助不大。面試

單例模式懶漢式 的寫法(DCL)是能夠檢驗你對 volatile 的瞭解,這也是面試中被問頻率較高的問題。小程序

本文將會介紹以下內容:緩存

  • volatile 的可見性是什麼,有什麼用
  • volatile 禁止指令重排是個什麼東東
  • 單例模式,餓漢式和懶漢式(DCL)的寫法,分析 (DCL)
  • 僞共享是什麼,怎麼避免僞共享

volatile

可見性

image-20200620152106412

計算機 CPU主存 交互的邏輯大體如圖,CPU 的運算速度是 主存 的 100 倍左右,爲了不 CPU 被主存拖慢速度。當CPU 須要一個數據的時候,會先從 L1 找,找到直接使用;L1 中未找到,會去 L2 中,L2 中找不到會去 L3 ,L3 找不到再去主存加載到 L3,再從 L3加載到 L2 ,再從 L2 加載到 L1安全

這樣提升的計算速度,同時也面臨數據不一致問題。微信

主存中如今有一個變量 a=1,CPU1 a+1 以後,將結果 a=2 放入到 L1 去,可是後續代碼計算還會用到 a,這時 CPU1 不會將 a=2 同步到主存中去。以後 CPU2 也從主存中取出變量 a(a=1),CPU2 將 a+2 的結果放入到 L1 中。這樣就形成了數據不一致問題。緩存一致性協議就是爲了解決這個問題的。性能

以上是計算機底層的實現原理,JAVA 在本身的虛擬機中執行,也有本身的內存模型,但無論怎麼樣,底層仍是依靠的 CPU 指令集達到緩存一致性。JAVA 的內存模型屏蔽了不一樣平臺緩存一致性協議的不一樣實現細節,定義了一套本身的內存模型。spa

image-20200620153620353

java 虛擬機中的變量所有儲存在主存中,每一個線程都有本身的工做內存,工做內存中的變量是主存變量的副本拷貝(使用那些變量,拷貝那些),每一個線程只會操做工做內存的變量,當須要保存數據一致性的時候,線程會將工做內存中的變量同步到主存中去。volatile 就是讓線程改變了 a 以後,回寫到主存中,已達到緩存一致。線程

接下來代碼體會一下,帶不帶 volatile 的區別。code

public class VolatileDemo {
    private static  int a = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (a == 0) {
            }
        }, "線程 1").start();
        System.out.println("修改 a=1 以前");
        Thread.sleep(3000);
        a = 1;
        System.out.println("修改 a=1 以後");
    }
}

運行這個程序,代碼會一直運行,不會中止。這是由於 線程1 的工做內存 a 爲 0,而主線程儘管修改了 a,但不會達到線程1從新加載主存中的變量 a。

public class VolatileDemo {
    // 代碼的區別只是加了 volatile
    private static volatile int a = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (a == 0) {

            }
        }, "線程 1").start();
        System.out.println("修改 a=1 以前");
        Thread.sleep(3000);
        a = 1;
        System.out.println("修改 a=1 以後");
    }
}

打印 修改 a=1 以後 程序中止。這是由於 volatile 標記的變量 a,主線程修改以後,並同步回主存,當其餘的線程再使用變量 a 的時候,java 內存模型會讓線程從主存加載變量 a。這就是 volatile可見性 特色。

禁止指令重排

java 中的字節碼最終都會編譯成機器碼(CPU 指令)執行,CPU 在保證單線程中執行結果不變的狀況下,能夠對指令進行指令重排已達到提升執行效率。

public class VolatileOrdering2 {
    static  int b = 1;
    public static void main(String[] args) throws InterruptedException {
        int a = 0;
        b = 2;
        a += 1;
        System.out.println(a);
    }
}

上述代碼指令重排執行順序的可能:

int a=0;
a+=1;
System.out.println(a);
int b = 2;

網上也有人寫的 demo 驗證可能會發生指令重排的小程序

public class T04_Disorder {
    private static int x = 0, y = 0;
    private static int a = 0, b =0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for(;;) {
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread one = new Thread(new Runnable() {
                public void run() {
                    //因爲線程one先啓動,下面這句話讓它等一等線程two. 讀着可根據本身電腦的實際性能適當調整等待時間.
                    //shortWait(100000);
                    a = 1;
                    x = b;
                }
            });
            Thread other = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    y = a;
                }
            });
            one.start();other.start();
            one.join();other.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                //System.out.println(result);
            }
        }
    }
    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }
}

假設指令重排不會發生,那麼 result 將不會打印,實際循環 n 次以後會打印 result

volatile 能夠禁止指令重排。

image-20200620174915155

大體簡單理解,加了內存屏障以後,代碼分紅 1,2,3部分。1 部分代碼你怎麼指令重排我無論,可是 1 部分代碼執行完了以後,必須執行 2 部分代碼,再執行 3 部分代碼。

單例模式

餓漢式

public class SingletonDemo {
    private static final SingletonDemo INSTANCE = new SingletonDemo();
    private SingletonDemo() {
    }
    public static SingletonDemo getInstance() {
        return SingletonDemo.INSTANCE;
    }
}

通常項目中咱們用這種用法便可,簡單方便,也沒誰閒着無聊利用別的手段給你打破單例。

懶漢式

餓漢式無論你用不用這個單例,只要類加載,單例就給你初始化好了。有的人就想讓其懶加載,節約那可憐的內存,用的時候單例再實例化。

public class SingletonDemo1 {
    private SingletonDemo1() {
    }

    public static SingletonDemo1 getInstance() {
        System.out.println("SingletonDemo1Holder 類加載");
        return SingletonDemo1Holder.getInstance();
    }

    private static class SingletonDemo1Holder {
        private static final SingletonDemo1 INSTANCE = new SingletonDemo1();
        public static SingletonDemo1 getInstance() {
            return SingletonDemo1Holder.INSTANCE;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        System.out.println(SingletonDemo1.getInstance());
        System.out.println(SingletonDemo1.getInstance());
    }
}

運行的時候加上這個 -XX:+TraceClassLoading 會打印加載的類。

image-20200620182245333

從圖中咱們能夠看到調用 SingletonDemo1.getInstance() 的時候,才加載的 SingletonDemo1Holder 類,再實例化單例,達到懶加載的要求。

DCL 實現單例

以上單例的實現看着沒啥技術含量,下面介紹一下 DCL (Double-checked locking),雙重檢查鎖的實現,這也是面試會問到的點。

public class SingletonDemo2 {
    // 考點在這裏,要不要加 volitale
    private volatile static SingletonDemo2 INSTANCE;
    private SingletonDemo2() {

    }
    public static SingletonDemo2 getInstance() {
        if (INSTANCE == null) {
            synchronized (SingletonDemo2.class) {
                if (INSTANCE == null) {
                    // 對象實例化
                    INSTANCE = new SingletonDemo2();
                }
            }
        }
        return INSTANCE;
    }
}

對象實例化實際能夠分爲幾個步驟:

一、分配對象空間

二、初始化對象

三、將對象指向分配的內存空間

當指令重排的時候,2 和 3 會進行重排序,致使有的線程可能拿到未初始化的對象調用,存在風險問題。

僞共享

volatile 給咱們帶來了變量 可見性 的功能,可是當使用不當,會掉入另外一個 僞共享 的坑。先看 demo.

public class VolatileDemo3 {
    private static volatile Demo[] demos = new Demo[2];
//    @sun.misc.Contended
    private static final class Demo {
        private volatile long x = 0L;
    }
    static {
        demos[0] = new Demo();
        demos[1] = new Demo();
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (long i = 0; i < 10000_0000L; i++) {
                demos[0].x = i;
            }
        });
        Thread thread = new Thread(() -> {
            for (long i = 0; i < 10000_0000L; i++) {
                demos[1].x = i;
            }
        });
        long start = System.nanoTime();
        thread.start();
        thread1.start();
        thread.join();
        thread1.join();
        long end = System.nanoTime();
        long runSecond = (end - start) / 100_0000;
        System.out.println("運行毫秒:" + runSecond);
    }
}

上述代碼,存在僞共享的狀況,我電腦運行 運行毫秒:2764

// 運行的時候,須要加上參數 -XX:-RestrictContended
public class VolatileDemo3 {
    private static volatile Demo[] demos = new Demo[2];
    @sun.misc.Contended
    private static final class Demo {
        private volatile long x = 0L;
    }
    static {
        demos[0] = new Demo();
        demos[1] = new Demo();
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (long i = 0; i < 10000_0000L; i++) {
                demos[0].x = i;
            }
        });
        Thread thread = new Thread(() -> {
            for (long i = 0; i < 10000_0000L; i++) {
                demos[1].x = i;
            }
        });
        long start = System.nanoTime();
        thread.start();
        thread1.start();
        thread.join();
        thread1.join();
        long end = System.nanoTime();
        long runSecond = (end - start) / 100_0000;
        System.out.println("運行毫秒:" + runSecond);
    }
}

上述代碼,使用 @sun.misc.Contended 避免僞共享,我電腦運行 運行毫秒:813

類似的用法在 ConcurrentHashMap 能夠看到,

@sun.misc.Contended 
static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}

上述代碼展現了僞共享會下降代碼的運行速度。什麼是僞共享呢。

還記得 Cpu 中的 L1 L2 L3 嗎,主存中的數據加載到 Cpu 的高速緩存的最小單位就是 緩存行(64 bit)。Cpu 的緩存失效,也是以緩存行爲單位失效。

Cpu 從內存加載數據的時候,它會把可能會用到的數據和目標數據一塊兒加載到 L1/L2/L3 中。上述代碼的變量 private static volatile Demo[] demos = new Demo[2]; 這兩個變量被一塊兒加載到同一個緩存行中去了,一個線程修改了其中的 demos[0].x 致使緩存行失效,另外一個線程修改 demos[1].x = i; 的時候發現緩存行失效,會去主存從新加載新的數據,兩個線程相互影響致使不停從內存加載,運行速度天然下降了。

@sun.misc.Contended 做用就是讓其單獨在一個緩存行中去。

咱們也能夠經過對齊填充,而避免僞共享。

緩存行 一般都是 64 bit。而 long 爲 8 個 bit,咱們本身補充 7 個沒有用 long 變量就可讓 x 和 7個沒用的變量單獨一個緩存行

public class VolatileDemo3 {
    private static volatile Demo[] demos = new Demo[2];
    private static final class Demo {
        private volatile long x = 0L;
        // 緩存行對齊填充的無用數據
        private volatile long pading1, pading2, pading3, pading4, pading5, pading6, pading7;
    }
    static {
        demos[0] = new Demo();
        demos[1] = new Demo();
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (long i = 0; i < 10000_0000L; i++) {
                demos[0].x = i;
            }
        });
        Thread thread = new Thread(() -> {
            for (long i = 0; i < 10000_0000L; i++) {
                demos[1].x = i;
            }
        });
        long start = System.nanoTime();
        thread.start();
        thread1.start();
        thread.join();
        thread1.join();
        long end = System.nanoTime();
        long runSecond = (end - start) / 100_0000;
        System.out.println("運行毫秒:" + runSecond);
    }


}

本文由 張攀欽的博客 http://www.mflyyou.cn/ 創做。 可自由轉載、引用,但需署名做者且註明文章出處。

如轉載至微信公衆號,請在文末添加做者公衆號二維碼。微信公衆號名稱:Mflyyou

相關文章
相關標籤/搜索