Java線程安全之volatile詳解
volatile
volatile 的存在,解決了不同內存間拷貝的同步問題,在每一次使用或者修改時候,都去原持有內存中去拿最新的狀態(tài)
或者說可以這樣理解,volatile 的修飾讓線程放棄了使用各自內存的做法轉而使用共享內存,從而保證了可靠性,但是降低了效率
所以說我們只在需要不同線程訪問同一個值的時候才去打開這個限制
第一種情況
首先我們來寫一個測試代碼
public class Synchronized1Test { private boolean running = true; public void runTest() { new Thread() { @Override public void run() { while (running) { System.out.println("線程開始執(zhí)行,,,,"); } } }.start(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } stop(); } private void stop() { running = false; } }
代碼大致要表達的是,主線程在一秒之后要調用 stop 方法將 running = false,這樣,子線程的死循環(huán)就會被打破,這樣子線程停止執(zhí)行,主線程也執(zhí)行完畢,整個程序結束。
但是實際的執(zhí)行結果是,子線程的死循環(huán)并不會結束,會一直不停的執(zhí)行下去
這是因為線程直接做變量的值的修改并不是直接改的,而是先拷貝一份到自己線程的內存區(qū)域,更改完之后再適時同步給原持有此值的線程,這樣做的目的是提高程序的運行效率
而在這里,主線程持有此變量,子線程在執(zhí)行時候,將值拷貝了一份到自己的內存中,然后子線程并沒有做值的修改,并不存在將值同步給主線程的過程,而此值的持有線程是主線程,主線程在更改完此值之后只會同步給自己,并不會同步給子線程,所以說子線程的值永遠為最初拷貝走的值,永遠為true,就永遠不會停下來了。
有一個關鍵字 volatile 能解決這個問題
public class Synchronized1Test { private volatile boolean running = true; ..... }
volatile 的存在,解決了不同內存間拷貝的同步問題,在每一次使用或者修改時候,都去原持有內存中去拿最新的狀態(tài),或者說可以這樣理解,volatile 的修飾讓線程放棄了使用各自內存的做法轉而使用共享內存,從而保證了可靠性,但是降低了效率,所以說我們只在需要不同線程訪問同一個值的時候才去打開這個限制
再次運行,果然一秒結束
第二種情況
public class Synchronized2Test { private int x = 0; private void count() { x++; } public void runTest() { new Thread() { @Override public void run() { for (int i = 0; i < 1_000_000; i++) { count(); } System.out.println("x 在線程1的最終值" + x); } }.start(); new Thread() { @Override public void run() { for (int i = 0; i < 1_000_000; i++) { count(); } System.out.println("x 在線程2的最終值" + x); } }.start(); } }
從代碼來看,兩個線程分別對同一個數進行一百萬次從自增操作,無論誰最后執(zhí)行完成,理論上來講,總有一個線程打印出來的結果是兩百萬,但實際的運行效果,無論執(zhí)行多少次都沒有兩百萬的結果出來。
都是因為上文說的,值的同步性問題,在各自內存區(qū)域修改完后適時同步回去,導致的這個問題
然后我們加上 volatile 關鍵字
public class Synchronized2Test { private volatile int x = 0; ..... }
運行,還是不行,為啥啊,都加上了怎么還是不行呢?
這里主要的原因是因為 x++; 這個操作
在 Java 中, x++; 是兩步操作,不是原子操作,是可拆的操作
他的運算過程大致相當于是
int temp = x + 1; x = temp;
這樣,在代碼分為兩行執(zhí)行的時候,搶線程的操作就發(fā)生了,就出現了值異常的情況
到目前為止,線程安全除了加 volatile 關鍵字外,還需要保證會互相影響的操作合成一個原子操作
這里解釋一下原子操作,簡單來說,把多個步驟看成一個整體,要么你別執(zhí)行我,要么就完全執(zhí)行,不要在執(zhí)行一半時候發(fā)生線程切換
這里引入我們保證一塊代碼塊執(zhí)行原子操作的關鍵字:synchronized
這時候我們改一下我們的代碼,給 count 方法增加 synchronized 關鍵字,并且可以將 volatile 關鍵字去除了
public class Synchronized2Test { private int x = 0; private synchronized void count() { x++; } ..... }
運行,果然順利的打印出來了兩百萬,多次運行也是這個結果
受保護的類型
到此,我們講一下官方提供的原子型的類型,這里拿之前出現過的 AtomicInteger 舉例
AtomicInteger 對標的就是 int,他是對 int 的包裝,可以保證 int 具有同步性和原子性
這里的同步性可以理解為上文提到的被 volatile 修飾,原子性可以理解為被 synchronized 修飾
例:
// 可以理解為默認值為 0 的 int AtomicInteger count = new AtomicInteger(0); // 相當于是 ++count count.incrementAndGet(); // 相當于是 count++ count.getAndIncrement();
代碼中的 count.incrementAndGet() 相當于是 ++count
count.getAndIncrement() 相當于是 count++
除此之外還有
常規(guī)類型 | 保護類型 |
int | AtomicInteger |
boolean | AtomicBoolean |
int[] | AtomicIntegerArray |
T | AtomicReference< T> |
… | … |
不一一列舉
其中要說的就是 AtomicReference,他把傳入的范型,自動幫我們做成了保護類型,使得在賦值和取值時候不出錯
到此這篇關于Java線程安全之volatile詳解的文章就介紹到這了,更多相關Java的volatile內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
你知道在Java中Integer和int的這些區(qū)別嗎?
最近面試,突然被問道,說一下Integer和int的區(qū)別.額…可能平時就知道寫一些業(yè)務代碼,包括面試的一些Spring源碼等,對于這種特別基礎的反而忽略了,導致面試的時候突然被問到反而不知道怎么回答了.哎,還是乖乖再看看底層基礎,順帶記錄一下把 ,需要的朋友可以參考下2021-06-06Spring線程池ThreadPoolExecutor配置并且得到任務執(zhí)行的結果
今天小編就為大家分享一篇關于Spring線程池ThreadPoolExecutor配置并且得到任務執(zhí)行的結果,小編覺得內容挺不錯的,現在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03SpringBoot和MybatisPlus實現通用Controller示例
本文主要介紹了SpringBoot和MybatisPlus實現通用Controller示例,只需創(chuàng)建實體類和mapper接口,就可以實現單表的增刪改查操作,具有一定的參考價值,感興趣的可以了解一下2025-03-03