深入詳細瞭解synchronized底層原理
歡迎大家搜尋“小猴子的技術筆記”關注我的公眾號,有問題可以及時和我交流。
在多執行緒之間,共享變數的值是執行緒不安全的,因為執行緒在開始執行之後都會擁有自己的工作空間,而從自己工作空間把修改的值重新整理回主存的時候需要CPU的排程。因此,一個執行緒看到的變數可能並不是最新的。
我們假設有個Share類中存放了一個共享的變數“count”。
public class Share {
public int count = 10000;
public void decrement() {
count--;
}
public int getCount() {
return count;
}
}
然後有兩個執行緒可以對這個共享的變數進行操作,每個執行緒都呼叫了5000次“decrement()”方法類進行共享變數的值修改:
public class ShareThread01 implements Runnable{
private Share share;
public AccountThread01(Share share) {
this.share = share;
}
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
share.decrement();
}
}
}
public class ShareThread02 implements Runnable{
private Share share;
public AccountThread02(Share share) {
this.share = share;
}
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
share.decrement();
}
}
}
如果上面的程式碼按照預期的執行,那麼最後的結果應該是0。請執行下面的程式碼進行驗證:
public class ShareTest {
public static void main(String[] args) throws InterruptedException {
while(true) {
Share share = new Share();
Thread t1 = new Thread(new ShareThread01(share));
Thread t2 = new Thread(new ShareThread02(share));
t1.start();
t2.start();
TimeUnit.SECONDS.sleep(2);
System.out.println(share.getCount());
}
}
}
執行上面的程式,你會發現每次輸出的結果是不一樣的。因為文章開頭已經說過,這是由於Java在多個執行緒同時訪問同一個物件的成員變數的時候,每個執行緒都擁有了這個物件變數的拷貝。因此在程式執行的過程中,一個執行緒所看到的變數並不一定是最新的。
也許你想到了使用之前學習的“volatile”關鍵字來使共享變數進行記憶體可見,保證執行緒安全。於是上述程式改成了如下:
public class ShareThread02 implements Runnable{
private volatile Share share;
public ShareThread02(Share share) {
this.share = share;
}
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
share.decrement();
}
}
}
然後再次執行測試程式也會發現,所輸出的結果並不是你想要的結果。請記住:關鍵字“volatile”只是保證了多執行緒之間共享變數的記憶體可見性,它並不保證共享變數的原子性。
這個時候關鍵字“synchronized”就派上了用場。它可以保證,同一個時刻只有一個執行緒能夠訪問被“synchronized”修飾的方法或者程式碼塊。將上述程式碼改成下面這樣:
public class Share {
public int count = 10000;
public synchronized void decrement() {
count--;
}
public int getCount() {
return count;
}
}
再次執行測試程式,這個時候你會發現,每次得到的結果都是一樣的。那麼為什麼新增了關鍵字“synchronized”之後就能夠按照預期的進行執行呢?
通過執行“javap -v Share.class”來看看底層做了哪些修飾:
可以看到,在方法上進行了關鍵字”synchronized“的修飾,底層的實現是標記了一個"ACC_SYNCHRONIZED"的標識。程式碼如果遇到了這個標識,就表示獲取到了物件的監視器monitor(monitor物件是由C++實現的),這個獲取的過程是排他的,也就是同一時刻只能有一個執行緒獲取到由synchronized所保護物件的監視器。
除此之外,關鍵字”synchronized“還可以對程式碼塊進行加鎖:
public class Share {
public int count = 10000;
public void decrement() {
synchronized (Share.class) {
count--;
}
}
public int getCount() {
return count;
}
}
將上述程式碼執行“javap -v Share.class”反編譯之後,可以看到如下:
”synchronized“關鍵字鎖程式碼塊的時候他提供了“monitor enter”和“monitor exit”兩個JVM指令,它能夠保證任何時候執行緒執行到“monitor enter”成功之前都必須從主記憶體中獲取資料。“monitor exit”退出之後,共享變數被更新後的值重新整理到主記憶體中,因此”synchronized“關鍵字還可以保證記憶體的可見性。
如果你仔細觀察就會發現,15和21有兩個“monitor exit”那麼為什麼會有兩個“monitor exit”呢?我們做這樣一個假設:如果執行緒啟動執行的過程中突然遇到異常了,這個時候執行緒該怎麼辦呢?總不能一直持有鎖吧!於是執行緒就會釋放鎖。因此,第二個“monitor exit”是為了執行緒遇到異常之後釋放鎖而準備的。
相關文章
- 深入瞭解Synchronized原理synchronized
- synchronized底層原理synchronized
- 詳解鎖原理,synchronized、volatile+cas底層實現synchronized
- volatile底層原理詳解
- HashMap原理詳解,包括底層原理HashMap
- 簡單瞭解InnoDB底層原理
- 深入詳解Java反射機制與底層實現原理?Java反射
- 基礎篇:詳解鎖原理,volatile+cas、synchronized的底層實現synchronized
- 詳細瞭解 synchronized 鎖升級過程synchronized
- 深入瞭解Redis底層資料結構Redis資料結構
- 三分鐘深入瞭解Spring底層Spring
- 面試題深入解析:Synchronized底層實現面試題synchronized
- Golang WaitGroup 底層原理及原始碼詳解GolangAI原始碼
- mysql學習筆記-底層原理詳解MySql筆記
- synchronized底層揭祕synchronized
- 深入理解 MySQL 索引底層原理MySql索引
- iOS 開發:『Runtime』詳解(三)Category 底層原理iOSGo
- Redis資料結構SortedSet底層原理詳解Redis資料結構
- Hive底層原理:explain執行計劃詳解HiveAI
- 探究synchronized底層原理(基於JAVA8原始碼分析)synchronizedJava原始碼
- python進階(16)深入瞭解GIL鎖(最詳細)Python
- 深入瞭解Zookeeper核心原理
- Java併發容器,底層原理深入分析Java
- 死磕synchronized底層實現synchronized
- 規範使用執行緒池與底層原理詳解執行緒
- 詳細瞭解 InnoDB 記憶體結構及其原理記憶體
- iOS底層系統:BSD層詳解iOS
- 【得物技術】深入理解synchronzied底層原理
- 切片底層陣列詳解陣列
- 高效能的Redis之物件底層實現原理詳解Redis物件
- 圖解Go的channel底層原理圖解Go
- OC底層探索(十六) KVO底層原理
- 詳細剖析分散式微服務架構下網路通訊的底層實現原理(圖解)分散式微服務架構圖解
- ConcurrentHashMap底層原理HashMap
- 深入瞭解Kafka基本原理Kafka
- Java類集框架詳細彙總-底層分析Java框架
- 死磕Synchronized底層實現--概論synchronized
- 死磕Synchronized底層實現–概論synchronized