JMM內(nèi)存模型

Volatile

什么是 Volatile

能夠保證線程可見性,當(dāng)一個(gè)線程修改共享變量時(shí),能夠保證對(duì)另外一個(gè)線程可見性,

但是注意他不能夠保證共享變量的原子性問題。

Volatile的特性

可見性

能夠保證線程可見性,當(dāng)一個(gè)線程修改共享變量時(shí),能夠保證對(duì)另外一個(gè)線程可見性,

但是注意他不能夠保證共享變量的原子性問題。

public class Mayikt extends Thread {
    /**
     * lock 鎖 匯編的指令 強(qiáng)制修改值,立馬刷新主內(nèi)存中 另外線程立馬可見刷新主內(nèi)存數(shù)據(jù)
     */
    private static volatile boolean FLAG = true;

    @Override
    public void run() {
        while (FLAG) {

        }
    }

    public static void main(String[] args) throws InterruptedException {
        new Mayikt().start();
        Thread.sleep(1000);
        FLAG = false;
    }
}

順序性

程序執(zhí)行程序按照代碼的先后順序執(zhí)行。

原子性

即一個(gè)操作或者多個(gè)操作 要么全部執(zhí)行并且執(zhí)行的過程,要么失敗。

CPU多核硬件架構(gòu)剖析

CPU每次從主內(nèi)存讀取數(shù)據(jù)比較慢,而現(xiàn)代的CPU通常涉及多級(jí)緩存,CPU讀主內(nèi)存

按照空間局部性原則加載 局部快到緩存中。

圖片.png

為什么會(huì)產(chǎn)生可見性的原因

因?yàn)槲覀僀PU讀取主內(nèi)存共享變量的數(shù)據(jù)時(shí)候,效率是非常低,所以對(duì)每個(gè)CPU設(shè)置

對(duì)應(yīng)的高速緩存 L1、L2、L3 緩存我們共享變量主內(nèi)存中的副本。

相當(dāng)于每個(gè)CPU對(duì)應(yīng)共享變量的副本,副本與副本之間可能會(huì)存在一個(gè)數(shù)據(jù)不一致性的問題。

比如線程線程B修改的某個(gè)副本值,線程A的副本可能不可見。導(dǎo)致可見性問題。

JMM內(nèi)存模型

Java內(nèi)存模型定義的是一種抽象的概念,定義屏蔽java程序?qū)Σ煌牟僮飨到y(tǒng)的內(nèi)存訪問差異。

主內(nèi)存

存放我們共享變量的數(shù)據(jù)

工作內(nèi)存

每個(gè)CPU對(duì)共享變量(主內(nèi)存)的副本。堆+方法區(qū)

JMM八大同步規(guī)范

圖片.png

(1)lock(鎖定):作用于 主內(nèi)存的變量,把一個(gè)變量標(biāo)記為一條線程獨(dú)占狀態(tài)

(2)unlock(解鎖):作用于 主內(nèi)存的變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定

(3)read(讀取):作用于 主內(nèi)存的變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的 工作內(nèi)存中,以便隨后的load動(dòng)作使用

(4)load(載入):作用于 工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中

(5)use(使用):作用于 工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎

(6)assign(賦值):作用于 工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量

(7)store(存儲(chǔ)):作用于 工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到 主內(nèi)存中,以便隨后的write的操作

(8)write(寫入):作用于 工作內(nèi)存的變量,它把store操作從工作內(nèi)存中的一個(gè)變量的值傳送到 主內(nèi)存的變量中

Volatile匯編lock指令

  1. 將當(dāng)前處理器緩存行數(shù)據(jù)立刻寫入主內(nèi)存中。

  2. 寫的操作會(huì)觸發(fā)總線嗅探機(jī)制,同步更新主內(nèi)存的值。

Volatile的底層實(shí)現(xiàn)原理

通過匯編lock前綴指令觸發(fā)底層鎖的機(jī)制

鎖的機(jī)制兩種:總線鎖(老機(jī)器一般都是這個(gè))/MESI緩存一致性協(xié)議

主要幫助我們解決多個(gè)不同cpu之間三級(jí)緩存之間數(shù)據(jù)同步

總線鎖

當(dāng)一個(gè)cpu(線程)訪問到我們主內(nèi)存中的數(shù)據(jù)時(shí)候,往總線總發(fā)出一個(gè)Lock鎖的信號(hào),其他的線程不能夠?qū)υ撝鲀?nèi)存做任何操作,變?yōu)樽枞麪顟B(tài)。該模式,存在非常大的缺陷,就是將并行的程序,變?yōu)榇?,沒有真正發(fā)揮出cpu多核的好處。

MESI協(xié)議

1.M 修改 (Modified) 這行數(shù)據(jù)有效,數(shù)據(jù)被修改了,和主內(nèi)存中的數(shù)據(jù)不一致,數(shù)據(jù)只存在于本Cache中。

2.E 獨(dú)享、互斥 (Exclusive) 這行數(shù)據(jù)有效,數(shù)據(jù)和主內(nèi)存中的數(shù)據(jù)一致,數(shù)據(jù)只存在于本Cache中。

3.S 共享 (Shared) 這行數(shù)據(jù)有效,數(shù)據(jù)和主內(nèi)存中的數(shù)據(jù)一致,數(shù)據(jù)存在于很多Cache中。

4.I 無效 (Invalid) 這行數(shù)據(jù)無效。

E:獨(dú)享:當(dāng)只有一個(gè)cpu線程的情況下,cpu副本數(shù)據(jù)與主內(nèi)存數(shù)據(jù)如果

保持一致的情況下,則該cpu狀態(tài)為E狀態(tài) 獨(dú)享。

S:共享:在多個(gè)cpu線程的情況了下,每個(gè)cpu副本之間數(shù)據(jù)如果保持一致

的情況下,則當(dāng)前cpu狀態(tài)為S

M:如果當(dāng)前cpu副本數(shù)據(jù)如果與主內(nèi)存中的數(shù)據(jù)不一致的情況下,則當(dāng)前cpu狀態(tài)

為M

I: 總線嗅探機(jī)制發(fā)現(xiàn) 狀態(tài)為m的情況下,則會(huì)將該cpu改為i狀態(tài) 無效

該cpu緩存主動(dòng)獲取主內(nèi)存的數(shù)據(jù)同步更新。

總線:維護(hù)解決cpu高速緩存副本數(shù)據(jù)之間一致性問題。

如果狀態(tài)是M的情況下,則使用嗅探機(jī)制通知其他的CPU工作內(nèi)存副本狀態(tài)為I無效狀態(tài),則 刷新主內(nèi)存數(shù)據(jù)到本地中,從而多核cpu數(shù)據(jù)的一致性。

為什么Volatile不能保證原子性

public class VolatileAtomThread extends Thread {

    private static volatile int count;

    public static void create() {
        count++;
    }

    public static void main(String[] args) {
        ArrayList<Thread> threads = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Thread tempThread = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    create();
                }
            });
            threads.add(tempThread);
            tempThread.start();
        }
        threads.forEach(thread -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(count);

    }
}

Volatile為了能夠保證數(shù)據(jù)的可見性,但是不能夠保證原子性,及時(shí)的將工作內(nèi)存的數(shù)據(jù)刷新主內(nèi)存中,導(dǎo)致其他的工作內(nèi)存的數(shù)據(jù)變?yōu)闊o效狀態(tài),其他工作內(nèi)存做的count++操作等于就是無效丟失了,這是為什么我們加上Volatile count結(jié)果在小于10000以內(nèi)。

JMM中的重排序及內(nèi)存屏障

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


    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        while (true) {
            i++;
            a = 0;
            b = 0;
            x = 0;
            y = 0;

            // a=1 x=b (x=0,y=1, y=0,x=1 x=0 y=0 x=1 ,y=1)
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {

                    a = 1;
                    x = b;
                }
            });
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {

                    b = 1;
                    y = a;
                }
            });
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
            System.out.println("第" + i + "次(" + x + "," + y + ")");
            if (x == 0 & y == 0) {0
                break;
            }
        }
    }
}

什么是重排序

Java內(nèi)存模型允許編譯器和處理器對(duì)指令代碼實(shí)現(xiàn)重排序提高運(yùn)行的效率,只會(huì)對(duì)不存在的數(shù)據(jù)依賴的指令實(shí)現(xiàn)重排序,在單線程的情況下重排序保證最終執(zhí)行的結(jié)果與程序順序執(zhí)行結(jié)果一致性。

重排序產(chǎn)生的原因

當(dāng)我們的CPU寫入緩存的時(shí)候發(fā)現(xiàn)緩存區(qū)正在被其他cpu站有的情況下,為了能夠提高CPU處理的性能可能將后面的讀緩存命令優(yōu)先執(zhí)行。

注意:不是隨便重排序,需要遵循as-ifserial語義。

as-ifserial:不管怎么重排序(編譯器和處理器為了提高并行的效率)

單線程程序執(zhí)行結(jié)果不會(huì)發(fā)生改變的。
也就是我們編譯器與處理器不會(huì)對(duì)存在數(shù)據(jù)依賴的關(guān)系操作做重排序。

CPU指令重排序優(yōu)化的過程存在問題

as-ifserial 單線程程序執(zhí)行結(jié)果不會(huì)發(fā)生改變的,但是在多核多線程的情況下

指令邏輯無法分辨因果關(guān)系,可能會(huì)存在一個(gè)亂序中心問題,導(dǎo)致程序執(zhí)行結(jié)果錯(cuò)誤。

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


    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        while (true) {
            i++;
            a = 0;
            b = 0;
            x = 0;
            y = 0;

            // a=1 x=b (x=0,y=1, y=0,x=1 x=0 y=0 x=1 ,y=1)
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {

                    a = 1;
                    x = b;
                }
            });
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {

                    b = 1;
                    y = a;
                }
            });
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
            System.out.println("第" + i + "次(" + x + "," + y + ")");
            if (x == 0 & y == 0) {
                break;
            }
        }
    }
}

內(nèi)存屏障解決重排序

處理器提供了兩個(gè)內(nèi)存屏蔽指令,解決以上存在的問題

1.寫內(nèi)存屏障:在指令后插入Stroe Barrier ,能夠讓寫入緩存中的最新數(shù)據(jù)更新寫入

主內(nèi)存中,讓其他線程可見。

這種強(qiáng)制寫入主內(nèi)存,這種現(xiàn)實(shí)調(diào)用,Cpu就不會(huì)因?yàn)樾阅艿目紤]對(duì)指令重排序。

2.讀內(nèi)存屏障:在指令前插入load Barrier ,可以讓告訴緩存中的數(shù)據(jù)失效,強(qiáng)制

從新主內(nèi)存加載數(shù)據(jù)

強(qiáng)制讀取主內(nèi)存,讓cpu緩存與主內(nèi)存保持一致,避免緩存導(dǎo)致的一致性問題。

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


    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        while (true) {
            i++;
            a = 0;
            b = 0;
            x = 0;
            y = 0;

            // a=1 x=b (x=0,y=1, y=0,x=1 x=0 y=0 x=1 ,y=1)
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {

                    a = 1;
                    x = b;
                }
            });
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {

                    b = 1;
                    y = a;
                }
            });
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();
            System.out.println("第" + i + "次(" + x + "," + y + ")");
            if (x == 0 & y == 0) {
                break;
            }
        }
    }


}


手動(dòng)插入內(nèi)存屏障

public class UnSafeUtils {

    public static Unsafe getUnsafe() {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            return (Unsafe) theUnsafe.get(null);
        } catch (Exception e) {
            return null;
        }
    }

}
 Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 1;
                    //插入寫內(nèi)存屏障
                    try {
                        // 手動(dòng)插入一個(gè)內(nèi)存屏障
                        UnSafeUtils.getUnsafe().storeFence();
                    } catch (Exception e) {

                    }
                    x = b;
                }
            });


雙重檢驗(yàn)鎖為什么需要加上volatile

public class Singleton03 {
    private static volatile Singleton03 singleton03;

    public static Singleton03 getInstance() {
        // 第一次檢查
        if (singleton03 == null) {
            //第二次檢查
            synchronized (Singleton03.class) {
                if (singleton03 == null) {
                    singleton03 = new Singleton03();
                }
            }
        }
        return singleton03;
    }

    public static void main(String[] args) {
        Singleton03 instance1 = Singleton03.getInstance();
        Singleton03 instance2 = Singleton03.getInstance();
        System.out.println(instance1==instance2);
    }
}

注意:因?yàn)槲覀冊(cè)趎ew操作 singleton03 = new Singleton03(),存在重排序的問題。

可以采用 javap -c 查看字節(jié)碼

  1. 分配對(duì)象的內(nèi)存空間

memory=allocate();

  1. 調(diào)用構(gòu)造函數(shù)初始化

  2. 將對(duì)象復(fù)制給變量

  3. 第二步和第三步流程存在重排序也有可能先執(zhí)行我們的,將對(duì)象復(fù)制給變量,在執(zhí)行

調(diào)用構(gòu)造函數(shù)初始化,導(dǎo)致另外一個(gè)線程獲取到該對(duì)象不為空,但是該改造函數(shù)沒有初始化,

所以就報(bào)錯(cuò)了 。就是另外一個(gè)線程拿到的是一個(gè)不完整的對(duì)象。

volatile存在的偽共享的問題

Cpu會(huì)以緩存行的形式讀取主內(nèi)存中數(shù)據(jù),緩存行的大小為2的冪次數(shù)字節(jié),

一般的情況下是為64個(gè)字節(jié)。

如果該變量共享到同一個(gè)緩存行,就會(huì)影響到整理性能。

例如:線程1修改了long類型變量A,long類型定義變量占用8個(gè)字節(jié),在由于

緩存一致性協(xié)議,線程2的變量A副本會(huì)失效,線程2在讀取主內(nèi)存中的數(shù)據(jù)的時(shí)候,

以緩存行的形式讀取,無意間將主內(nèi)存中的共享變量B也讀取到內(nèi)存中,而化主內(nèi)存

中的變量B沒有發(fā)生變化。

圖片.png

public class FalseShareTest implements Runnable {
    // 定義4和線程
    public static int NUM_THREADS = 4;
    // 遞增+1
    public final static long ITERATIONS = 500L * 1000L * 1000L;
    private final int arrayIndex;
    // 定義一個(gè) VolatileLong數(shù)組
    private static VolatileLong[] longs;
    // 計(jì)算時(shí)間
    public static long SUM_TIME = 0l;

    public FalseShareTest(final int arrayIndex) {
        this.arrayIndex = arrayIndex;
    }

    public static void main(final String[] args) throws Exception {
        for (int j = 0; j < 10; j++) {
            System.out.println(j);
            if (args.length == 1) {
                NUM_THREADS = Integer.parseInt(args[0]);
            }
            longs = new VolatileLong[NUM_THREADS];
            for (int i = 0; i < longs.length; i++) {
                longs[i] = new VolatileLong();
            }
            final long start = System.nanoTime();
            runTest();
            final long end = System.nanoTime();
            SUM_TIME += end - start;
        }
        System.out.println("平均耗時(shí):" + SUM_TIME / 10);
    }

    private static void runTest() throws InterruptedException {
        Thread[] threads = new Thread[NUM_THREADS];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new FalseShareTest(i));
        }
        for (Thread t : threads) {
            t.start();
        }
        for (Thread t : threads) {
            t.join();
        }
    }

    public void run() {
        long i = ITERATIONS + 1;
        while (0 != --i) {
            longs[arrayIndex].value = i;
        }
    }

    //    @sun.misc.Contended
    public final static class VolatileLong extends AbstractPaddingObject {
        public volatile long value = 0L;
//        public long p1, p2, p3, p4, p5, p6;
    }
}

使用緩存行填充方案避免為共享

Jdk1.6中實(shí)現(xiàn)方案

public final static class VolatileLong{
    public volatile long value = 0L;
    public  long p1, p2, p3, p4, p5, p6;

}

定義p1-6 加上value 一共占用56個(gè)字節(jié) ,在加上VolatileLong類中頭占用8個(gè)字節(jié)一共就是占用64個(gè)字節(jié)。

注意:在Jdk1.7開始對(duì)該代碼做優(yōu)化了,會(huì)導(dǎo)致p1-p6無效,所以必須要寫一個(gè)類單獨(dú)繼承。

Jdk1.7中實(shí)現(xiàn)方案

public final static class VolatileLong extends AbstractPaddingObject {
    public volatile long value = 0L;
    public  long p1, p2, p3, p4, p5, p6;
}
public class AbstractPaddingObject {
    public  long p1, p2, p3, p4, p5, p6;
}

@sun.misc.Contended

可以直接在類上加上該注解@sun.misc.Contended,啟動(dòng)的時(shí)候需要加上該參數(shù)-XX:-RestrictContended

  1. ConcurrentHashMap中的CounterCell
圖片.png

synchronized 與volatile存在的區(qū)別

1.Volatile保證線程可見性,當(dāng)工作內(nèi)存中副本數(shù)據(jù)無效之后,主動(dòng)讀取主內(nèi)存中數(shù)據(jù)

2.Volatile可以禁止重排序的問題,底層內(nèi)存屏障。

3.Volatile不會(huì)導(dǎo)致線程阻塞,不能夠保證線程安全問題,synchronized 會(huì)導(dǎo)致線程阻塞

能夠保證線程安全問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容