JVM運行時數據區和CPU優化以及線程原子操作

一、JVM運行時數據區

在這裏插入圖片描述
線程獨佔: 每個線程都會有它獨立的空間,隨線程生命週期而創建和銷燬。
線程共享: 所有線程能訪問這塊內存數據,隨虛擬機或者GC而創建和銷燬。
方法區: 用於存儲已被虛擬機家族的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
Java堆內存: Java堆是被所有線程共享的一塊內存區域,此內存的唯一目的就是存放對象,幾乎所有的對象實例都在這裏分配內存。
**虛擬機棧:**虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀用於存儲舉報變量表、操作數棧、動態鏈接、方法出口等信息。每個方法從調用到執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到處棧的過程。

棧幀:虛擬機棧由多個棧幀(Stack Fframe)組成。一個線程會執行一個或多個方法,一個方法對應一個棧幀。

本地方法棧: 和虛擬機棧功能類似,虛擬機棧是爲虛擬機執行JAVA方法而準備的,本地方法棧是爲虛擬機使用Native本地方法而準備的。
程序計數器: 記錄當前線程執行字節碼的位置,字節碼解釋器工作時,就是通過改變計數器的值來選取下一條需要執行的字節碼指令。如果執行java方法,存儲的是字節碼指令地址,如果執行Native方法,則計數器值爲空。

直接內存: 直接內存不是JVM運行時數據區的一部分,也不是《Java虛擬機規範》中定義的內存區域,是我們常說的堆外內存。

二、CPU性能優化手段

1、緩存

爲了提高程序運行的性能,現代CPU在很多方面對程序進行了優化。
例如:CPU高速緩存。儘可能地避免處理器訪問主內存的時間開銷,處理器大多會利用緩存(cache)以提高性能。
在這裏插入圖片描述
CPU高速緩存中的數據是內存中的一小部分,但這一小部分是短時間內CPU即將訪問的,當CPU調用大量數據時,就可避開內存直接從Cache中調用。

CPU在讀取數據時,先在L1中尋找,再從L2尋找,再從L3尋找,然後是內存,再後是外存儲器。

緩存同步協議

在這種高速緩存回寫的場景下,有一個緩存一致性協議(MESI協議)多數CPU廠商對它進行了實現。
多處理器時,單個CPU對緩存中數據進行了改動,需要通知給其他CPU。也就是意味着,CPU處理要控制自己的讀寫操作,還要監聽其他CPU發出的通知,從而保證最終一致。

2、運行時指令重排

在這裏插入圖片描述
指令重排的場景:當CPU寫緩存時發現緩存區塊正被其他CPU佔用,爲了提高CPU處理性能,可能將後面的讀緩存命令優先執行。

as-if-serial語義:指令重排時,不管怎麼重排序,單個線程的執行結果不能被改變。

換句話說,編譯器和處理器不會對存在數據依賴關係的操作做重排序。

Java編程語言的語義允許Java編譯器和微處理器進行執行優化,這些優化導致了與其交互的代碼不再同步,從而導致看似矛盾的行爲。
在這裏插入圖片描述

優化帶來的問題

a、 CPU高速緩存下有一個問題:
緩存中的數據與主內存的數據並不是實時同步的,各CPU(或CPU核心)間緩存的數據也不是實時同步。在同一個時間點,各CPU所看到同一內存地址的數據的值可能是不一致的。

b、 CPU執行指令重排序優化下有一個問題:
雖然遵守了as-if-serial語義,單僅在單CPU自己執行的情況下能保證結果正確。
多核多線程中,指令邏輯無法分辨因果關聯,可能出現亂序執行,導致程序運行結果錯誤。
在這裏插入圖片描述

3、內存屏障

處理器提供了兩個內存屏障指令(Memory Barrier)用於解決上述兩個問題:

寫內存屏障(Store Memory Barrier): 在寫指令後插入Store Barrier,能讓寫入緩存中的最新數據更新寫入主內存,讓其他線程可見。

強制寫入主內存,這種顯示調用,CPU就不會因爲性能考慮而去對指令重排。

**讀內存屏障(Load Memory Barrier):**在讀指令前插入Load Barrier,可以讓高速緩存中的數據失效,強制從新從主內存加載數據。

強制讀取主內存內容,讓CPU緩存與主內存保持一致,避免了緩存導致的一致性問題

三、多線程中的問題

1、 所見非所得
2、 無法肉眼去檢測程序的準確性
3、 不同的運行平臺有不同的表現
4、 錯誤很難重現

package com.dongnao.concurrent.period3;

public class Demo1Visibility {
    int i = 0;
    boolean isRunning = true;

    public static void main(String args[]) throws InterruptedException {
        Demo1Visibility demo = new Demo1Visibility();

        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("here i am...");
                while(demo.isRunning){
                    demo.i++;
                }
                System.out.println("i=" + demo.i);
            }
        }).start();

        Thread.sleep(3000L);
        demo.isRunning = false;
        System.out.println("shutdown...");
    }
}

上面這個程序在不同環境下運行結果是不一樣的:
在這裏插入圖片描述

四、原子操作

1、什麼是原子操作:

原子操作可以是一個步驟,也可以是多個操作步驟,但是其順序不可以被打亂,也不可以被切割而只執行其中的一部分(不可中斷性)。

將整個操作視作一個整體,資源在該次操作中保持一致,這是原子性的核心特徵。

例如:i++不是原子操作,存在競態條件,線程不安全,需要轉變爲原子操作才能安全。

2、競態條件與臨界區

競態條件: 當兩個線程競爭同一資源時,如果對資源的訪問順序敏感,就稱存在競態條件。
臨界區: 就是導致靜態條件發生的代碼區。

下面舉例一個多線程調用 i++ 的例子:

public class Demo1_CounterTest {

    public static void main(String[] args) throws InterruptedException {
        final Counter ct = new Counter();

        for (int i = 0; i < 6; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        ct.add();
                    }
                    System.out.println("done...");
                }
            }).start();
        }

        Thread.sleep(6000L);
        System.out.println(ct.i);
    }
}
public class Counter {
    volatile int i = 0;

    public void add() {
        i = i + 1;
    }
}

啓動6個線程,每個線程都加10000次,如果是線程安全的,i最後應該是60000,但實際輸出是:

done...
done...
done...
done...
done...
done...
32039

這說明volatile修飾的i是線程共享的,但不是線程安全的,不具備原子性。
把Counter改成下面的類就是線程安全的,結果就是我們預期的效果:

public class CounterLock {
    volatile int i = 0;

    Lock lock = new ReentrantLock();

    public void add() {
        lock.lock();
        i++;
        lock.unlock();
    }
}
public class CounterSync {
    volatile int i = 0;

    public synchronized void add() {
        i++;
    }
}
public class CounterAtomic {
    AtomicInteger at = new AtomicInteger(0);

    public void add(){
        at.getAndIncrement();
    }

    public int getValue(){
        return at.get();
    }
}

package com.dongnao.concurrent.period4;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class CounterUnsafe {
    int i = 0;

    //Unsafe工具類非常強大,特可以去修改引用類型的值,可以修改對象的屬性、可以修改數組 等等
    private static Unsafe unsafe = null;

    //代表了要修改的字段 是一個偏移量
    private static long  valueOffset;
    
    static {
        //unsafe = Unsafe.getUnsafe();
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            //指定要修改的字段
            Field iFiled = CounterUnsafe.class.getDeclaredField("i");
            valueOffset = unsafe.objectFieldOffset(iFiled);

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }


    public void add(){
        //i++;      //普通的java語法實心的操作,沒辦法保證原子性
        for(;;){
            if (unsafe.compareAndSwapInt(this, valueOffset, i, i+1)){
                return;
            }
        }
    }

}

3、CAS(Compare and swap)機制

1、CAS 屬於硬件同步原語,處理器提供的內存操作指令,保證原子性
2、CAS 操作需要兩個參數,一箇舊值和一個目標值,修改前先比較舊值是否改變,如果沒變,將新值賦給變量,否則不做改變。
補充:JAVA中的sun.misc.Unsafe類提供了CAS機制。
在這裏插入圖片描述

CAS存在的問題

  1. 僅針對單個變量的操作,不能用於多個變量來實現原子操作。
  2. 循環+CAS,自旋的實現讓所有線程都處於高頻運行,爭搶CPU執行時間的狀態 。如果操作長時間不成功,會帶來很大的CPU資源消耗。
  3. ABA問題。(無法體現出數據的變動)。

在這裏插入圖片描述

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