文章目錄
一、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存在的問題
- 僅針對單個變量的操作,不能用於多個變量來實現原子操作。
- 循環+CAS,自旋的實現讓所有線程都處於高頻運行,爭搶CPU執行時間的狀態 。如果操作長時間不成功,會帶來很大的CPU資源消耗。
- ABA問題。(無法體現出數據的變動)。