併發程式設計之JMM模型&Volatile底層原理
併發程式設計的本質
併發程式設計在一定的意義上其實就是多執行緒程式設計,那麼多執行緒程式設計中主要有哪些,
多執行緒程式設計中會涉及到同步操作,執行緒互斥操作,大的任務進行拆解併發處理(分工);
所以併發程式設計的實質就是為了解決我們的業務需求,從而儘可能的壓榨CPU的效能,達到效能的最大化;
可以說在現在的大多數應用程式中,多執行緒的身影無處不在,單執行緒的應用已經成為過去時,如果非要說多執行緒的應用場景是什麼,我可以說多執行緒的應用場景無處不在,適用於任何的場景下。
JMM模型
JMM模型就是JAVA的記憶體模型,但是這裡我更喜歡叫JAVA的執行緒記憶體模型,
我們都知道我們的JVM的記憶體模型中有堆,元空間還有虛擬機器棧,虛擬機器棧由執行引擎執行時會在虛擬機器棧中建立棧楨來儲存我們的變數資訊和操作資訊,所以JMM模型和虛擬機器就有很大的關係,
當我們建立一個物件時,比如new User()和 new Thread()這兩者有什麼區別呢?
相同點: 都是在堆區建立一個java物件,
不同點: 我們都知道Thread是java的執行緒物件,通過它可以啟動一個執行緒去執行我們的任務,而User只是一個普通的java物件,那麼他們兩者有什麼區別呢?
當我們建立一個執行緒Thread,其實不是JAVA開闢了一個執行緒,Thread底層啟動執行緒和執行緒的操作是呼叫的native本地方法的,所以其實建立執行緒是交給了核心去
做的,JVM本身是不具備排程CPU的許可權的 ,是由核心去排程CPU的,比如看下圖:
說白了就是JVM不具備呼叫CPU的許可權,是交給了作業系統去執行的,實現從使用者態向核心態的轉變。
我們都知道JAVA應用程式可以在windows、linux、unix等作業系統上執行,但是我們編寫的多執行緒程式只有一份,也沒有在程式中區分作業系統而執行不同的程式碼塊,所以java應用程式建立執行緒交給作業系統核心的時候這個時候是遮蔽了不同作業系統的差異的,我們可以把它抽象出來,(抽象出來就是我們不管作業系統的型別,我寫的這個多執行緒在不同的作業系統上都能執行,並且都能執行成功而達到預期。 這點很重要 jmm 為了遮蔽各種硬體
和作業系統對記憶體訪問的差異性而規定的)
併發程式設計帶來的風險
併發程式設計是好,併發程式設計會帶來效能的提升,但是它就真的好,無腦使用嗎?
答案是不是的!
併發程式設計還是要根據實際的業務場景和熟練的程度來使用,否則就會帶來一定的風險,那麼會帶來哪些風險?
1.效能的問題:
就是說我們的業務場景是否需要啟動多執行緒來執行我們的這些任務,這個要根據資料量和處理的任務複雜度來考慮,比如有個需求可以用單執行緒執行的,你非要使用多執行緒來執行,那麼效能不一定會提升,反而會降低系統性能,為什麼呢?因為多執行緒是要考慮到執行緒間的上下文切換帶來的效能損耗的;是不是系統出現了一些問題,會有人經常給你說,這段程式操作的原因是因為多執行緒的上下文切換帶來的效能開銷導致了一些問題;而單執行緒是一直佔用cpu的執行許可權的,它可以一直執行,直到執行完成,所有是否採用多執行緒還是要根據要處理的任務的複雜度以及資料量的大小,多執行緒在這種情況下就不一定會達到效能提升的效果,我們來看下多執行緒的上下文切換的圖:
所以要根據實際的情況考慮是否採用多執行緒,多執行緒是好,可以儘可能的壓榨CPU的效能,但是多執行緒會有執行緒的上下文切換,所以這個要考慮到效能的開銷來覺得是否採用多執行緒。
2.執行緒的活躍性問題:
執行緒的活躍性問題有飢餓,死鎖和活鎖
- 飢餓:就是在多執行緒的環境下,比如有些執行緒被調了執行緒的優先順序非常低,那麼有可能這個執行緒永遠都不會被排程,永遠處於飢餓的狀態,類似於死鎖一樣,永遠不會被排程;所以這種情況下,要特別注意這種情況的發生,也就是說執行緒的優先順序是可以影響到執行緒獲取CPU執行週期,一個執行緒啟動了永遠得不到CPU的執行時間週期,那麼就會被活活餓死。
- 死鎖:死鎖這個比較好理解,就是Thrad1鎖了a物件,依賴b物件,而Thrad2鎖了b物件,依賴a物件,那麼就會出現相互依賴,永遠無法釋放物件鎖,就會出現死鎖
public class T0915 {
final static Object a = new Object();
final static Object b = new Object();
public static void main(String[] args) throws InterruptedException {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"開始執行...");
synchronized (a){
System.out.println(Thread.currentThread().getName()+" 獲得a物件執行...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b){
System.out.println(Thread.currentThread().getName()+" 依賴b物件執行...");
}
}
},"Thread1").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"開始執行...");
synchronized (b){
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 獲得b物件執行...");
synchronized (a){
System.out.println(Thread.currentThread().getName()+" 依賴a物件執行...");
}
}
},"Thread2").start();
}
}
- 活鎖:
活鎖是什麼意思呢?活鎖就是說你獲取了鎖沒有意義,就是說你獲取了鎖沒有幹正事兒,比如兩個執行緒,第一個執行緒獲取了鎖,發現需要讓步給其他鎖,其他鎖獲取了鎖,也讓給了其他鎖,這樣一來一往,其實大家都沒有幹正事兒
public class LiveLockTest {
/**
* 定義一個勺子,ower 表示這個勺子的擁有者
*/
static class Spoon {
Diner owner;
public Spoon(Diner diner) {
this.owner = diner;
}
public String getOwnerName() {
return owner.getName();
}
public void setOwner(Diner diner) {
this.owner = diner;
}
//表示正在用餐
public void use() {
System.out.println(owner.getName() + " 用這個勺子吃飯.");
}
}
/**
* 定義一個晚餐類
*/
static class Diner {
private boolean isHungry;
//用餐者的名字
private String name;
public Diner(boolean isHungry, String name) {
this.isHungry = isHungry;
this.name = name;
}
//和某人吃飯
public void eatWith(Diner diner, Spoon sharedSpoon) {
try {
synchronized (sharedSpoon) {
while (isHungry) {
//當前用餐者和勺子擁有者不是同一個人,則進行等待
while (!sharedSpoon.getOwnerName().equals(name)) {
sharedSpoon.wait();
}
if (diner.isHungry()) {
System.out.println(diner.getName()
+ " 餓了," + name + "把勺子給他.");
sharedSpoon.setOwner(diner);
sharedSpoon.notifyAll();
} else {
//用餐
sharedSpoon.use();
sharedSpoon.setOwner(diner);
isHungry = false;
}
Thread.sleep(500);
}
}
} catch (InterruptedException e) {
System.out.println(name + " is interrupted.");
}
}
public boolean isHungry() {
return isHungry;
}
public void setHungry(boolean hungry) {
isHungry = hungry;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static void main(String[] args) {
final Diner ant = new Diner(true, "ant");
final Diner monkey = new Diner(true, "monkey");
final Spoon sharedSpoon = new Spoon(monkey);
Thread h = new Thread(()->ant.eatWith(monkey, sharedSpoon));
h.start();
Thread w = new Thread(()->monkey.eatWith(ant, sharedSpoon));
w.start();
// try {
// Thread.sleep(10000);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// h.interrupt();
// w.interrupt();
}
}
3.執行緒的資料安全問題:
執行緒資料安全問題就是比較常見而且也是多執行緒程式設計的一個老話題了,
針對執行緒安全問題,一般通過鎖來控制執行緒的資料安全,鎖機制在java多執行緒開發過程中大體分為兩種,內建鎖和JUC鎖:
內建鎖:使用Synchornized,synchronized是java的關鍵字,如果簡簡單單的通過看底層原始碼的方式是沒有辦法看到的,需要通過彙編指令來看才能理解這個鎖的機制,
synchronized鎖的物件,不是程式碼塊,這個概念一定要弄清楚,因為經常會聽到說鎖程式碼塊,鎖方法,其實它是鎖的物件;
synchronized鎖是不需要程式設計師釋放的,它本身會自己釋放鎖,底層有一個鎖升級的過程,依次為自適應鎖(自旋鎖)、偏向鎖、輕量級鎖、重量級鎖,
synchronized是基於object monitor的機制來實現的,預設是非公平鎖。
JUC包下面的鎖:JUC下面的鎖比較常用的有ReentrantLock和LockSupport,這個鎖是JDK的已有的實現,
JUC鎖有獨佔鎖、共享鎖、公平鎖、非公平鎖以及讀寫鎖;
JUC鎖都有一個特徵如果你手動加鎖了,記得要手動釋放,jvm是不會給你釋放鎖的,一般我們會在finally中去釋放鎖,保證整個鎖能夠釋放。
不管什麼語言,併發的程式設計都是在高階的部分,可見併發有多難,因為併發的涉及的知識太廣,不單單是作業系統的知識,還有計算機的組成的知識等等。說到底,這些年硬體的不斷的發展,但是一直有一個核心的矛盾在:CPU、記憶體、I/O裝置的三者的速度的差異。這就是所有的併發的源頭
為了那麼解決這三者的差異生產的解決辦法如下:
-
CPU增加了快取,以均衡與記憶體的差異;
-
作業系統增加了程序、執行緒,以分時複用CPU,進而均衡CPU與I/O裝置的速度差異;
-
編譯程式優化指令執行次序,使得快取能夠得到更加合理的利用。
但是 以上的解決辦法都會導致相應的問題。 由此併發的三大問題來源我們已經找到了
併發問題出現的三大源頭
2.1快取導致可見性問題
- 單核:所有的執行緒都是在一顆CPU上執行,CPU快取與記憶體的資料一致性容易解決。因為所有執行緒都是同一個CPU的快取,一個執行緒對快取的寫,對另外一個執行緒來說一定是可見的。
多核:每顆CPU都有自己的快取,這時CPU快取與記憶體的資料一致性就沒有那麼容易解決了,當多個執行緒在不同的CPU上執行時,這些執行緒操作的是不同的CPU快取(CPU的解決的方案:MESI協議)
可見性問題深入分析
思考:上面例子中為什麼多執行緒對共享變數的操作存在可見性問題?
我們主要看load 方法和 flag變數 的編碼方式
threadB修改flag,但是threadA 不能跳出 while迴圈的情況有以下兩種
/**
* @author Fox
*
* -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
*/
public class VisibilityTest {
private boolean flag = true;
private int i = 0;
public void refresh() {
flag = false;
System.out.println(Thread.currentThread().getName() + "修改flag");
}
public void load() {
System.out.println(Thread.currentThread().getName() + "開始執行.....");
int i = 0;
while (flag) {
i++;
//TODO 業務邏輯
}
System.out.println(Thread.currentThread().getName() + "跳出迴圈: i=" + i);
}
public static void main(String[] args) throws InterruptedException {
VisibilityTest test = new VisibilityTest();
// 執行緒threadA模擬資料載入場景
Thread threadA = new Thread(() -> test.load(), "threadA");
threadA.start();
// 讓threadA執行一會兒
Thread.sleep(1000);
// 執行緒threadB通過flag控制threadA的執行時間
Thread threadB = new Thread(() -> test.refresh(), "threadB");
threadB.start();
}
public static void shortWait(long interval) {
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
} while (start + interval >= end);
}
}
1、flag屬性定義如下 :
private boolean flag = true;
2、load方法如下
public void load() {
System.out.println(Thread.currentThread().getName() + "開始執行.....");
int i = 0;
while (flag) {
//TODO 業務邏輯
}
System.out.println(Thread.currentThread().getName() + "跳出迴圈: i=" + i);
}
執行結果為: thread A 在while 迴圈裡面執行 未終止
threadB修改flag,threadA能跳出 while迴圈的情況有以下:
1、使用volatile關鍵字修飾變數,保證變數的可見性
package bat.ke.qq.com.learnjuc.volatiledemo;
/**
* 原始碼學院-Fox
* 只為培養BAT程式設計師而生
* http://bat.ke.qq.com
* 往期視訊加群:516212256 暗號:6
*
* -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
*
*/
public class VisibilityTest {
// JMM模型 java執行緒記憶體模型
// 可見性 為什麼? lock addl $0x0,(%rsp) 觸發快取一致性協議
private volatile boolean flag = true;
private int i = 0;
public void refresh(){
flag = false;
System.out.println(Thread.currentThread().getName()+"修改flag");
}
public void load(){
System.out.println(Thread.currentThread().getName()+"開始執行.....");
int i=0;
while (flag){
i++;
}
System.out.println(Thread.currentThread().getName()+"跳出迴圈: i="+ i);
}
public static void main(String[] args){
VisibilityTest test = new VisibilityTest();
new Thread(() -> test.load(), "threadA").start();
try {
Thread.sleep(2000);
new Thread(()->test.refresh(),"threadB").start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}
}
1、flag屬性定義如下 :
private volatile boolean flag = true;
2、load 方法
public void load(){
System.out.println(Thread.currentThread().getName()+"開始執行.....");
while (flag){
i++;
}
System.out.println(Thread.currentThread().getName()+"跳出迴圈: i="+ i);
}
執行結果: thread A 跳出while 迴圈
2、使用記憶體屏障保證可見性
package bat.ke.qq.com.learnjuc.volatiledemo;
/**
* 原始碼學院-Fox
* 只為培養BAT程式設計師而生
* http://bat.ke.qq.com
* 往期視訊加群:516212256 暗號:6
*
* -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
*
*/
public class VisibilityTest {
private boolean flag = true;
private int i = 0;
public void refresh(){
flag = false;
System.out.println(Thread.currentThread().getName()+"修改flag");
}
public void load(){
System.out.println(Thread.currentThread().getName()+"開始執行.....");
while (flag){
i++;
// 使用記憶體屏障保證flag的可見性
UnsafeFactory.getUnsafe().storeFence();
}
System.out.println(Thread.currentThread().getName()+"跳出迴圈: i="+ i);
}
public static void main(String[] args){
VisibilityTest test = new VisibilityTest();
new Thread(() -> test.load(), "threadA").start();
try {
Thread.sleep(2000);
new Thread(()->test.refresh(),"threadB").start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}
}
1、flag屬性定義如下 :
private boolean flag = true;
2、load 方法
public void load(){
System.out.println(Thread.currentThread().getName()+"開始執行.....");
while (flag){
i++;
// 使用記憶體屏障保證flag的可見性
UnsafeFactory.getUnsafe().storeFence();
}
System.out.println(Thread.currentThread().getName()+"跳出迴圈: i="+ i);
}
執行結果: thread A 跳出while 迴圈
3、使用yeild方法釋放時間片 ,與上下文切換有關會重新去載入主記憶體的值
package bat.ke.qq.com.learnjuc.volatiledemo;/** * 原始碼學院-Fox * 只為培養BAT程式設計師而生 * http://bat.ke.qq.com * 往期視訊加群:516212256 暗號:6 * * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp * */public class VisibilityTest { private boolean flag = true; private int i = 0; public void refresh(){ flag = false; System.out.println(Thread.currentThread().getName()+"修改flag"); } public void load(){ System.out.println(Thread.currentThread().getName()+"開始執行....."); while (flag){ i++; Thread.yield(); } System.out.println(Thread.currentThread().getName()+"跳出迴圈: i="+ i); } public static void main(String[] args){ VisibilityTest test = new VisibilityTest(); new Thread(() -> test.load(), "threadA").start(); try { Thread.sleep(2000); new Thread(()->test.refresh(),"threadB").start(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); }}
1、flag屬性定義如下 :
private boolean flag = true;
2、load 方法
public void load(){ System.out.println(Thread.currentThread().getName()+"開始執行....."); while (flag){ i++; Thread.yield(); } System.out.println(Thread.currentThread().getName()+"跳出迴圈: i="+ i); }
執行結果: thread A 跳出while 迴圈
4、 使用System.out.println("====="); 列印語句可以跳出while迴圈
synchroinzed 可以保證可見性,為什麼可以保證可見性是因為 synchroinzed的底層會呼叫storefence 這個記憶體屏障
package bat.ke.qq.com.learnjuc.volatiledemo;/** * 原始碼學院-Fox * 只為培養BAT程式設計師而生 * http://bat.ke.qq.com * 往期視訊加群:516212256 暗號:6 * * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp * */public class VisibilityTest { private boolean flag = true; private int i = 0; public void refresh(){ flag = false; System.out.println(Thread.currentThread().getName()+"修改flag"); } public void load(){ System.out.println(Thread.currentThread().getName()+"開始執行....."); while (flag){ i++; System.out.println("====="); } System.out.println(Thread.currentThread().getName()+"跳出迴圈: i="+ i); } public static void main(String[] args){ VisibilityTest test = new VisibilityTest(); new Thread(() -> test.load(), "threadA").start(); try { Thread.sleep(2000); new Thread(()->test.refresh(),"threadB").start(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); }}
1、flag屬性定義如下 :
private boolean flag = true;
2、load 方法
public void load(){ System.out.println(Thread.currentThread().getName()+"開始執行....."); int i=0; while (flag){ i++; System.out.println("====="); } System.out.println(Thread.currentThread().getName()+"跳出迴圈: i="+ i); }
執行結果: thread A 跳出while 迴圈
5、使用LockSupport.unpark(Thread.currentThread());
public class VisibilityTest { private boolean flag = true; private int count = 0; public void refresh(){ flag = false; System.out.println(Thread.currentThread().getName()+"修改flag"); } public void load(){ System.out.println(Thread.currentThread().getName()+"開始執行....."); while (flag){ count++; LockSupport.unpark(Thread.currentThread()); } System.out.println(Thread.currentThread().getName()+"跳出迴圈: count="+ count); } public static void main(String[] args){ VisibilityTest test = new VisibilityTest(); new Thread(() -> test.load(), "threadA").start(); try { Thread.sleep(2000); new Thread(()->test.refresh(),"threadB").start(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); }}
1、flag屬性定義如下 :
private boolean flag = true;
2、load 方法
public void load(){ System.out.println(Thread.currentThread().getName()+"開始執行....."); while (flag){ count++; LockSupport.unpark(Thread.currentThread()); } System.out.println(Thread.currentThread().getName()+"跳出迴圈: count="+ count);}
6、i 關鍵字加上volatile關鍵字
public class VisibilityTest { private boolean flag = true; private volatile int i = 0; public void refresh(){ flag = false; System.out.println(Thread.currentThread().getName()+"修改flag"); } public void load(){ System.out.println(Thread.currentThread().getName()+"開始執行....."); while (flag){ i++; } System.out.println(Thread.currentThread().getName()+"跳出迴圈: i="+ i); } public static void main(String[] args){ VisibilityTest test = new VisibilityTest(); new Thread(() -> test.load(), "threadA").start(); try { Thread.sleep(2000); new Thread(()->test.refresh(),"threadB").start(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); }}
1、int 屬性定義如下 :
private volatile int i= 0;
2、load 方法
public void load(){ System.out.println(Thread.currentThread().getName()+"開始執行....."); while (flag){ i++; } System.out.println(Thread.currentThread().getName()+"跳出迴圈: count="+ count);}
執行結果;
7、使用Integer修飾i
package bat.ke.qq.com.learnjuc.volatiledemo;import java.util.concurrent.locks.LockSupport;/** * 原始碼學院-Fox * 只為培養BAT程式設計師而生 * http://bat.ke.qq.com * 往期視訊加群:516212256 暗號:6 * * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp * */public class VisibilityTest { // JMM模型 java執行緒記憶體模型 // 可見性 為什麼? lock addl $0x0,(%rsp) 觸發快取一致性協議// private volatile boolean flag = true; private boolean flag = true; private Integer i = 0; public void refresh(){ flag = false; System.out.println(Thread.currentThread().getName()+"修改flag"); } public void load(){ System.out.println(Thread.currentThread().getName()+"開始執行....."); while (flag){ i++; } System.out.println(Thread.currentThread().getName()+"跳出迴圈: i="+ i); } public static void main(String[] args){ VisibilityTest test = new VisibilityTest(); new Thread(() -> test.load(), "threadA").start(); try { Thread.sleep(2000); new Thread(()->test.refresh(),"threadB").start(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); }}
1、int 屬性定義如下 :
private Integer i= 0;
2、load 方法
public void load(){ System.out.println(Thread.currentThread().getName()+"開始執行....."); while (flag){ i++; } System.out.println(Thread.currentThread().getName()+"跳出迴圈: i="+ i);}
執行結果;
8、 使用showWait方法
package bat.ke.qq.com.learnjuc.volatiledemo;import java.util.concurrent.locks.LockSupport;/** * 原始碼學院-Fox * 只為培養BAT程式設計師而生 * http://bat.ke.qq.com * 往期視訊加群:516212256 暗號:6 * * -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp * */public class VisibilityTest { // JMM模型 java執行緒記憶體模型 // 可見性 為什麼? lock addl $0x0,(%rsp) 觸發快取一致性協議// private volatile boolean flag = true; private boolean flag = true; private int i = 0; public void refresh(){ flag = false; System.out.println(Thread.currentThread().getName()+"修改flag"); } public void load(){ System.out.println(Thread.currentThread().getName()+"開始執行.....");// int i=0; while (flag){ i++; // TODO 不能的情況 // 1. 不能 迷 為什麼? 取決於快取是否失效(過期) // shortWait(10000); // 10微秒 // TODO 能的情況: // 使用記憶體屏障保證flag的可見性// UnsafeFactory.getUnsafe().storeFence();// Thread.yield(); // 1.sleep 讓出cpu時間片 /*try { Thread.sleep(0); } catch (InterruptedException e) { e.printStackTrace(); }*/ // 2. synchronized 可見性保證 記憶體屏障// System.out.println("====="); // 3. 我們發現i++執行時間短,人為的延長執行時間 ,延長1000微妙 等於1 ms 毫秒 shortWait(1000000);// LockSupport.unpark(Thread.currentThread()); } System.out.println(Thread.currentThread().getName()+"跳出迴圈: i="+ i); } public static void main(String[] args){ VisibilityTest test = new VisibilityTest(); new Thread(() -> test.load(), "threadA").start(); try { Thread.sleep(2000); new Thread(()->test.refresh(),"threadB").start(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void shortWait(long interval){ long start = System.nanoTime(); long end; do{ end = System.nanoTime(); }while(start + interval >= end); }}
1、int 屬性定義如下 :
private int i= 0;
2、load 方法
public void load(){ System.out.println(Thread.currentThread().getName()+"開始執行....."); while (flag){ i++; // 模擬程式的執行時間。。。 // 我們發現i++執行時間短,人為的延長執行時間 ,延長1000微妙 等於1 ms 毫秒 shortWait(1000000); } System.out.println(Thread.currentThread().getName()+"跳出迴圈: i="+ i);}
執行結果;
Java記憶體模型(JMM)
JMM定義
Java虛擬機器規範中定義了Java記憶體模型(Java Memory Model,JMM),用於遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各種平臺下都能達到一致的併發效果,
JMM規範了Java虛擬機器與計算機記憶體是如何協同工作的:
- 規定了一個執行緒如何和何時可以看到由其他執行緒修改過後的共享變數的值,
- 以及在必須時如何同步的訪問共享變數。
**JMM描述的是一種抽象的概念,一組規則,通過這組規則控制程式中各個變數在共享資料區域和私有資料區域的訪問方式,
JMM即執行緒通訊是圍繞原子性、有序性、可見性展開的。
JMM(Java記憶體模型 )是一個共享記憶體模型 ,它與 執行緒之間通訊有關係。
從根本上來說,執行緒間的通訊有兩種方式:
-
一種是在訊息傳遞的併發模型裡,執行緒之間通過傳送訊息來進行通訊(沒有公共狀態) ----適用於協程
-
一種是在共享記憶體的併發模型中,執行緒之間通過讀-寫共享記憶體來實現通訊(公共狀態)--適用於java執行緒之間的通訊併發
這麼說有點不明白,那麼 我們舉個例子說明一下上面的兩種方式:
demo:執行緒A 與執行緒B 之間想要互動資料,執行緒A把 共享變數i改成5 ,執行緒B想要讀到共享變數i 的值
方式1: 在訊息傳遞的併發模型裡,執行緒之間通過傳送訊息來進行通訊(沒有公共狀態) -----適用於協程
就是 執行緒A把 共享變數i改成5 ,直接傳送訊息 告訴執行緒B ,執行緒B 收到訊息 可以直接拿到執行緒A修改過後的i值
方式2: 在共享記憶體的併發模型中,執行緒之間通過讀-寫共享記憶體來實現通訊(公共狀態)
但是很可惜協程是通過 上述方式1訊息傳遞的方式 ,Java的併發採用的是共享記憶體模型,所以Java執行緒之間的通訊是基於共享記憶體實現的
具體來講Java執行緒之間的通訊由Java記憶體模型控制,JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。如果執行緒A與B之間要通訊的話,必須經歷下面兩個步驟:
執行緒A 與執行緒B 之間想要互動資料,,必須經歷下面兩個步驟:
- 執行緒A把本地記憶體中更新過的共享變數重新整理到主記憶體中去。
- 執行緒B到住記憶體中去讀取執行緒A之前更新過的共享變數。
大家要理解這個jmm執行緒模型一定要站線上程與執行緒之間如何互動的角度去理解
jmm是一個抽象的概念,本地記憶體和 主記憶體不是真實存在的,操作的空間也是邏輯空間!!
並不是說有個硬體區域叫主記憶體、有個硬體區域叫做本地記憶體 ,後面講處理器架構會提到這塊。
站在Java記憶體角度的模型 , 我們開始分析 , threadA 為什麼可以跳出迴圈, 為什麼threadA 不可以跳出迴圈。。。
這邊還要說一個東西就是 就是關於主記憶體與工作記憶體之間的具體互動協議:
即一個變數如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步到主記憶體之間的實現細節,Java記憶體模型定義了以下八種操作來完成, 虛擬機器實現時必須保證這8種操作都是原子的、不可分割的(對於long和double型別的變數來說,load、store、read跟write在某些平臺上允許例外)。
記憶體互動操作
- lock(鎖定):作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔狀態。
- unlock(解鎖):作用於主記憶體變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
- read(讀取):作用於主記憶體變數,把一個變數值從主記憶體傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
- load(載入):作用於工作記憶體的變數,它把read操作從主記憶體中得到的變數值放入工作記憶體的變數副本中。
- use(使用):作用於工作記憶體的變數,把工作記憶體中的一個變數值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。
- assign(賦值):作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
- store(儲存):作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write的操作。
- write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中。
主記憶體、工作記憶體互動流程如下:
一:啟動執行緒A,因為現在處理器採用多核,所以執行緒A 中 run方法的程式碼由java執行引擎放在cpu core1 的暫存器內執行
cpu core1 暫存器需要執行while(flag) ,其中falg是全域性變數
① 執行緒A使用read指令,將主記憶體中的falg的變數值傳輸到執行緒A的工作記憶體中
- read(讀取):作用於主記憶體變數,把一個變數值從主記憶體讀取接著傳輸到執行緒的工作記憶體中,以便隨後的load動作使用
② 線上程A的工作記憶體中,使用load指令把 從主記憶體中得到的falg的變數值 放入工作記憶體的變數副本 (此時我們可以看到執行緒A的工作記憶體,有一份變數副本)
- load(載入):作用於工作記憶體的變數,它把read操作把從主記憶體中得到的變數值放入工作記憶體的變數副本中。
注意: 執行緒A的工作記憶體中是:
先使用read指令:獲取到變數值true之後 然後使用load指令 :將true賦值給變數副本 flag
③ 執行緒A有屬於自己的程式計數器,通過改變這個pc計數器來選取下一條需要執行的位元組碼指令 , 當pc計數器指向while(flag)指令時
執行引擎會將我們的我們的while(true)指令 指令載入到cpu的暫存器 中 執行
注意:flag 讀取的是執行緒A本地記憶體的flag
即 使用use指令將工作記憶體中flag副本值傳遞給執行引擎,執行引擎將其交給cpu執行,cpu執行while(true)指令
- use(使用):作用於工作記憶體的變數,,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作把工作記憶體中的一個變數值傳遞給執行引擎,然後執行引擎將齊指令交給cpu執行
二:2s過後,執行緒B啟動,
問: 執行緒B 啟動會在CPU CORE1 上執行嗎??
答:不會
這邊要涉及到作業系統的知識了:
Windows 採用基於優先順序的、搶佔排程演算法來排程執行緒。
Windows 排程程式 (用於處理排程的 Windows 核心部分稱為排程程式) 確保具有最高優先順序的執行緒總是在執行的。
- 排程程式選擇執行的執行緒會一直執行,直到被更高優先順序的執行緒所搶佔,或終止,或呼叫阻塞系統呼叫(如 I/O)。
- 低優先順序執行緒執行時,更高優先順序的實時執行緒變成就緒,那麼低優先順序執行緒就被搶佔。這種搶佔使得實時執行緒在需要使用 CPU 時優先得到使用。(實時執行緒
優先順序高) ,
而我們的執行緒A 執行 while(true),執行緒A是一個高優先順序的執行緒, 能保證執行緒A就算呼叫Thread.yeild 方法 暫時釋放cpu的執行權,也會在不久的將來重新獲取cpu的執行權,繼續執行while(true) 指令 。。 所以執行緒B 此時 會使用 cpu core2
問: 如果os的核數有限的話,高優先順序執行緒執行while(true) 容易造成死鎖問題 ?
答: 優先順序比較低的執行緒會因為優先順序低而搶佔不到cpu的資源造成死鎖,
比如說有3個執行緒 : 有一個 while(true)的高優先順序執行緒A , 有兩個優先順序低的的執行緒B、C 獲取同一把鎖lock
執行緒B 最先啟動, 並上鎖!!,執行緒C 啟動 阻塞在lock鎖上,,
此時執行緒A啟動,執行緒會一直佔用cpu 的執行權不釋放 , 那麼執行緒B 一直持有鎖, 執行緒C 想要獲取lock鎖從而一直阻塞在lock 鎖上 (執行緒B 釋放鎖才能解除執行緒A的阻塞,執行緒B 又一直獲取不到cpu執行權,所以一直持有lock鎖)
通過上面的講解我們知道執行緒B 在cpu core2 上執行 。。。。
執行緒B也是和執行緒A 一樣,從主記憶體中read flag ,然後 load flag 生成flag 副本,然後 使用use 指令, cpu將 修改flag為false ,
④⑤⑥ 參考上面的①②③步驟
三: ThreadB 更新flag 共享變數
⑦ cpu core2中 暫存器的值需要賦值給工作記憶體 :
cpu把flag的變數值交給執行引擎,然後cpu執行assign指令 把一個從執行引擎接收到的值賦值給工作記憶體的變數
- assign(賦值):作用於工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作,它把一個從執行引擎接收到的值賦值給工作記憶體的變數,
⑧ 執行緒B的工作記憶體會把flag變數副本值傳輸給主記憶體
- store(儲存):作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write的操作。
⑨ 將flag變數副本值賦值給主記憶體中的flag變數
- write(寫入):作用於主記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中。
問:執行緒B 更新完本地記憶體的變數副本flag 為 false的 , 會馬上使用store、write指令把本地記憶體flag 的變數值重新整理回主記憶體嗎?
答: 不會, 只有在某些條件執行了的情況下,才會立馬重新整理 。。
前致知識: jmm中說的本地記憶體一般都是使用硬體層面cpu的快取, jmm中說的主記憶體一般使用的就是 硬體層面的主記憶體
os對快取淘汰(jmm中的工作記憶體)有一些淘汰策略,在快取淘汰(也就是快取失效)之前才會把快取的資料同步主記憶體 (也就是同步jmm的主記憶體)
至於這個某些條件 後面說
問: 執行緒B 消亡之前 會使用store、write指令把本地記憶體副本值重新整理回主記憶體嗎?
答: 是的,執行緒B快要結束了,就表示flag的資料不再使用了,那麼本地記憶體中副本變數即將失效!!
那麼執行緒B結束之前肯定會把 快取的資料同步到主記憶體 (工作記憶體 同步到主記憶體)
問: 那什麼時候執行緒A的本地記憶體失效呢,這樣子執行緒A就可以去拿主記憶體的最新flag=false的資料,從而跳出 while迴圈了!!
現在是因為while(true) 本地記憶體會一直存在,不會失效。
那麼正常程式在使用快取的時候,快取一定會有使用時間的,不可能一直存在 。。因為我們的快取空間很小,當快取空間不夠使用的時候,一定會基於某種演算法淘汰掉一些無用的快取, 如果while(true){ xxxx} , xxx執行的邏輯超過50ms ,那麼變數副本flag 肯定會被淘汰,
上面的 8、執行緒A使用showWait 就是 因為這個原因 而跳出 while 迴圈的 。。。
你可以修改showwait(1000) 之後 ,那麼快取不會淘汰 ,,
於是我們需要採取一些策略去淘汰快取;
保證可見性問題 轉換成快取淘汰問題:
1、 使用 Thread.yeild 方法 ,
這個方法 會釋放時間片, 意味著 有個cpu上下問切換的概念,,
切換前 while(true) 儲存執行緒, 切換後 還原執行緒 ,會去載入執行緒的上下文 。。。執行緒上下文切換會導致快取失效
Thread1 執行 x=5 , y=6 ,z= x+ y , 執行刀 y= 6後 cpu切換到執行緒 2
執行緒2 執行完後 ,有切換回 執行緒1 ,執行緒1 繼續執行 z= x+y
??1、 x、y的值為 5、6 是從哪裡取得
2、 執行緒1 為什麼知道程式繼續從 z=x+y的地方執行
有一個叫做pc 暫存器,(硬體層面) ,可以知道執行位置
x,y 的值哪裡來???, thea1 切換之前
會儲存上下文資訊:如果x、y的值為寫操作,那麼就會把 xy的值重新刷回到主存中的某個地址 ,如果是讀操作 則直接清除快取即可
之後 thea1 切換後來後,會重新從主存中載入資料 (載入上下文)
於是 thread 讀到最新的flag資料 工作副本資料
執行緒上下文切換 時間為5-10 ms , 執行緒上下文切換 很耗費效能的 !!!
上下文存在哪裡跟硬體架構有關係 。。。。
之後還會再講的 ,,
程式在計算機上是如何執行的 。。。。
x=6, y= x+5
講了一下 計算機架構程式是怎樣執行的
vlotaile 為什麼可以跳出迴圈。。
Java關鍵字,要去看jvm原始碼。。。 c
看openjdk的原始碼 !!!
volatile在hotspot的實現:
位元組碼直譯器實現
JVM中的位元組碼直譯器(bytecodeInterpreter),用C++實現了JVM指令,其優點是實現相對簡單且容易理解,缺點是執行慢。
bytecodeInterpreter.cpp
storeLoad這是有個記憶體屏障,注意這個jmm層面的記憶體屏障, 不是處理器層面的記憶體屏障(fence)
jmm : storestore loadload storeload 。。
在linux系統x86中的實現
orderAccess_linux_x86.inline.hpp
inline void OrderAccess::storeload() { fence(); }inline void OrderAccess::fence() { if (os::is_MP()) {// 是否是多核的處理器架構 // always use locked addl since mfence is sometimes expensive#ifdef AMD64 __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");#else __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");#endif }}
jvm 可以在不同的處理器架構下執行,linux 、winodw 虛擬機器 跨平臺可以給我們遮蔽掉底層的不同的差異。
不同的處理器架構有不同的實現,,
我們看的是x86架構的實現,呼叫了fence()風法
lock; addl $0,0(%%rsp) 是一個彙編層面的指令,,, lock字首指令,, 是storeload的實現,
lock字首指令不是處理器架構中記憶體屏障的指令(mfence),
因為x86 架構認為lock字首指令的效能比mfence 記憶體屏障的處理機架構指令要好一些,所以總是喜歡用lock 替換
知道lock字首指令的作用就可以知道如何valtoile 為何 能夠保證可見性!!!
一下就幹到彙編,此時就跟硬體有關
lock指令的作用,, 重排序先不管,,,
第三點:3. LOCK字首指令會 等待它之前所有的指令完成、並且所有緩衝的寫操作寫回記憶體(也就是將store buffer中的內容寫入記憶體)之後才開始執行,並且根據快取一致性協議,重新整理store buffer的操作會導致其他cache中的副本失效。
什麼意識呢 ???
如何用了lock 指令會保證 ,lock指令是涉及到硬體的
我們都知道jmm 中的本地記憶體,主記憶體是邏輯空間,邏輯空間最終會對映到物理空間,
jmm會盡可能的保證 保證工作記憶體的資料對映到cpu快取 【cache】中,保證了主記憶體的資料對映到記憶體中。。
於是可見性可以保證了。。
lock指令不是一個cpu處理器中不是一個記憶體屏障指令,但是它有記憶體屏障的效果,還能讓處理器快取中的其他副本失效。。
快取失效從主記憶體中載入flag
lock字首指令能讓快取失效???為什麼?? 於硬體有關之後講
另外一種方式: jvm 做了優化把常用的方式用模板直譯器實現:
模板直譯器實現
模板直譯器(templateInterpreter),其對每個指令都寫了一段對應的彙編程式碼,啟動時將每個指令與對應彙編程式碼入口繫結,可以說是效率做到了極致。
templateTable_x86_64.cpp
// 負責執行putfield或putstatic指令void TemplateTable::putfield_or_static(int byte_no, bool is_static, RewriteControl rc) { // ... // Check for volatile store __ testl(rdx, rdx); __ jcc(Assembler::zero, notVolatile); putfield_or_static_helper(byte_no, is_static, rc, obj, off, flags); // x86只生效哦storeLoad volatile_barrier(Assembler::Membar_mask_bits(Assembler::StoreLoad | Assembler::StoreStore)); __ jmp(Done); __ bind(notVolatile); putfield_or_static_helper(byte_no, is_static, rc, obj, off, flags); __ bind(Done); }void TemplateTable::volatile_barrier(Assembler::Membar_mask_bits order_constraint) { // Helper function to insert a is-volatile test and memory barrier if (os::is_MP()) { // Not needed on single CPU __ membar(order_constraint); }}
assembler_x86.hpp
// Serializes memory and blows flags void membar(Membar_mask_bits order_constraint) { // We only have to handle StoreLoad // x86平臺只需要處理StoreLoad if (order_constraint & StoreLoad) { int offset = -VM_Version::L1_line_size(); if (offset < -128) { offset = -128; } // 下面這兩句插入了一條lock字首指令: lock addl $0, $0(%rsp) lock(); // lock字首指令 addl(Address(rsp, offset), 0); // addl $0, $0(%rsp) } }
下面這兩句插入了一條lock字首指令: lock addl $0, $0(%rsp)
lock(); // lock字首指令 :
lock addl $0, $0(%rsp) 再次驗證了 都會執行彙編指令
addl(Address(rsp, offset), 0); // addl $0, $0(%rsp) 在rsp是我們的64位暫存器,在我們的棧頂加入0 無意義
彙編層面volatile的實現 :
一定要有hsdis-amd65.ddl
新增下面的jvm引數檢視之前可見性Demo的彙編指令
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp
找到putfield flag 即可:
驗證了可見性使用了lock字首指令
前端編譯: jvm指令
後端編譯: 彙編指令
彙編指令最後會轉換成機器指令。。供計算機使用
UnsafeFactory.getUnsafe().storeFence();
x86 的 storefence -》 底層就是呼叫了
sout
底層用了Synchroinzed :x86 也是呼叫UnsafeFactory.getUnsafe().storeFence();
那麼 storefence 記憶體屏障---》 lock指令
park: x86 也是呼叫UnsafeFactory.getUnsafe().storeFence();
sleep --->呼叫parlk ->x86 -》x86 也是呼叫UnsafeFactory.getUnsafe().storeFence();
Integer: 於關鍵字 final 有關
value值 是一個final 關鍵字修飾, jvm 對final做了優化 保證了可見性
java可見性如何保證: 方式規律由兩種
可見性的問題:
1、 利用jmm 層面storeLoad ===》 x86 使用lock替代了mfence
2、利用執行緒上下文切換 ,快取失效。。 Thread.yeild
等待喚醒機制: park unpark
linux底層用的是 pthrea_cond_timewait
從硬體層面分析Lock字首指令
操作的規則:
java記憶體模型還規定了在執行上述8種基本操作時必須滿足如下規則:
- 不允許read和load、store和write操作之一單獨出現,即不允許載入或同步工作到一半。
- 不允許一個執行緒丟棄它最近的assign操作,即變數在工作記憶體中改變了之後,必須吧改變化同步回主記憶體。
- 不允許一個執行緒無原因地(無assign操作)把資料從工作記憶體同步到主記憶體中。
- 一個新的變數只能在主記憶體中誕生。
- 一個變數在同一時刻只允許一條執行緒對其進行lock操作,但lock操作可以被同一條執行緒重複執行多次,,多次lock之後必須要執行相同次數的unlock操作,變數才會解鎖。
- 如果對一個物件進行lock操作,那會清空工作記憶體變數中的值,在執行引擎使用這個變數前,需要重新執行load或assign操作初始化變數的值。
- 如果一個變數事先沒有被lock,就不允許對它進行unlock操作,也不允許去unlock一個被其他執行緒鎖住的變數。
- 對一個變數執行unlock操作之前,必須將此變數同步回主記憶體中(執行store、write)。
有如上8種記憶體訪問操作以及規則限定,再加上對volatile的一些特殊規定,就已經完全確定了java程式中哪些記憶體訪問操作是在併發下安全的。
對於volatile的特殊規則:
volatile有兩個特性:1、對所有執行緒可見;2、防止指令重排;我們接下來說明一下這兩個特性。
可見性,是指當一條執行緒修改了某個volatile變數的值,新值對於其它執行緒來說是可以立即知道的。而普通變數無法做到這點。但這裡有個誤區,由於volatile對所有執行緒立即可見,對volatile的寫操作會立即反應到其它執行緒,因此基於volatile的變數的運算在併發下是安全的。這是錯誤的,原因是volatile所謂的其它執行緒立即知道,是其它執行緒在使用的時候會讀讀記憶體然後load到自己工作記憶體,如果這時候其它執行緒進行了修改,本執行緒的volatile變數狀態會被置為無效,會重新讀取,但如果本執行緒的變數已經被讀入執行棧幀,那麼是不會重新讀取的;那麼兩個執行緒都把本地工作記憶體內容寫入主存的時候就會發生覆蓋問題,導致併發錯誤。
防止指令重排,重排序優化是機器級的操作,也就是硬體級別的操作。重排序會打亂程式碼順序執行,但會保證在執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,因此在一個執行緒的方法執行過程中無法感知到重排的操作影響,這也是“執行緒內表現為序列”的由來。volatile的遮蔽重排序在jdk1.5後才被修復。原理是volatile生成的彙編程式碼多了一條帶lock字首的空操作的命令,而根據IA32手冊規定,這個lock字首會使得本cpu的快取寫入記憶體,而寫入動作也會引起別的cpu或者別的核心無效化,這相當於對cpu快取中的變數做了一次store跟write的操作,所以通過這樣一個操作,可以讓變數對其它cpu立即可見(因為狀態被置為無效,用的話必須重新讀取)。
另外,java記憶體模型對volatile變數有三條特殊規則:
a、每次使用變數之前都必須先從主記憶體重新整理最新的值,用於保證能看見其它執行緒對變數的修改;
b、每次對變數修改後都必須立刻同步到主記憶體中,用於保證其它執行緒可以看到自己的修改;
c、兩個變數都是volatile的,將資料同步到記憶體的時候,先讀的先寫;
long跟double變數的特殊規則
對於64位的資料型別long跟double,java記憶體模型定義了一條相對寬泛的規定:允許虛擬機器將沒有被volatile修飾的64位資料操作分為兩次32位的操作來進行。也就是允許虛擬機器不保證64位資料load、store、read跟write這4個操作的原子性,這就是long跟double的非原子性協定。如果真的這樣,當多個執行緒共享一個並未宣告為volatile的long或者double型別的變數,並同時對他們進行讀取修改,那麼某些執行緒可能會讀到一些既非初始值也不是其他執行緒修改值的代表了“半個變數”的資料。
不過這種讀到“半個變數”的情況非常罕見,因為java記憶體模型雖然允許實現為非原子的但“強烈建議”將其實現為原子操作,實際開發中,所有商用虛擬機器都將其實現為原子操作,因此,這點我們並不需要擔心。
Java虛擬機器-pc暫存器執行過程理解 https://blog.csdn.net/Alphr/article/details/105665051
問題?在某個時間點本地記憶體會把資料刷回主記憶體?與硬體有關
可見性問題是因為資料在快取中的更新不能及時的通知其它執行緒。
二期;https://blog.csdn.net/qq_36434742/article/details/106722869
-
專題一--併發程式設計專題
-
提取碼:wtel
Effective Java 第三版——78. 同步訪問共享的可變資料
3期: https://blog.csdn.net/scjava/article/details/108673512?spm=1001.2014.3001.5501
併發程式設計之JMM模型&Volatile底層原理 https://blog.csdn.net/scjava/article/details/108673512
【併發程式設計系列3】volatile記憶體屏障及實現原理分析(JMM和MESI)
https://blog.csdn.net/zwx900102/article/details/106306915/?utm_medium=distribute.pc_relevant.none-task-blog-2defaultbaidujs_title~default-1.no_search_link&spm=1001.2101.3001.4242
4期: 4. synchronized詳解](https://www.cnblogs.com/ITPower/p/13620573.html) https://note.youdao.com/ynoteshare/index.html?id=ee257490aa10fc87bb8c3823ed1e5421&type=note&_time=1632375637449
https://www.cnblogs.com/IcanFixIt/p/10633368.html
VIP併發程式設計之JMM&volatile詳解
使用工具看彙編指令,是目前最好的教程說快取一致性的。。。
二期鎖:
jol openjdk ,下過來可以看到。。
64bits - > 8 個位元組
32bits ->4個位元組
128bits -> 12個位元組
鎖是給物件上鎖:
其實就是給物件頭的最後兩位上鎖
00、01、10、11 表示四種狀態
五種狀態通過2個bits表示
無鎖:前56個表示hashcode 、 1一個未使用 4個表示年齡 1個是否是偏向鎖的標誌 最後兩個表示鎖
ptr_to_lock_record 下節課說
不延遲了一上來就是偏向鎖,之後會升級
偏向鎖有缺點,偏向鎖要消除鎖
13-java記憶體模型
二期關於 hb 八大原則的!!https://vip.tulingxueyuan.cn/detail/v_60336242e4b035d3cdba00b0/3?from=p_603394e5e4b035d3cdba188e&type=6
①sleep方法給其他執行緒執行機會時不考慮執行緒的優先順序,因此會給低執行緒優先順序執行的機會,而yield方法只會給相同優先順序或者更高優先順序執行緒執行的機會
②執行緒執行sleep()方法後轉入阻塞狀態,所以,執行sleep()方法的執行緒在指定的時間內不會被執行,而yield()方法只是使當前執行緒重新回到可執行狀態,所以執行yield()方法的執行緒可能在進入可執行狀態後馬上又被執行
③sleep()方法宣告丟擲InterruptedException,而yield()方法沒有宣告任何異常
④sleep()方法比yield()方法(跟作業系統相關)有更好的可移植性
————————————————
版權宣告:本文為CSDN博主「不羈朔風」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連結及本宣告。
原文連結:https://blog.csdn.net/qq_36071795/article/details/83890281
JMM與硬體記憶體架構的關係
Java記憶體模型與硬體記憶體架構之間存在差異。硬體記憶體架構沒有區分執行緒棧和堆。
對於硬體,所有的執行緒棧和堆都分佈在主記憶體(這個主記憶體 叫做 RAM - main memory 是硬體中的叫法 )中。部分執行緒棧和堆可能有時候會出現在CPU快取中和CPU內部的暫存器中。如上圖所示,Java記憶體模型和計算機硬體記憶體架構是一個交叉關係 。。 這個敘述是不是有問題呀 ?? 晚上問一下老師