Java多線程之線程同步
volatile
先看個例子
class Test { // 定義一個全局變量 private boolean isRun = true; // 從主線程調(diào)用發(fā)起 public void process() { test(); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } stop(); } // 啟動一個子線程循環(huán)讀取isRun private void test() { new Thread(new Runnable() { @Override public void run() { while (isRun) { // 疑問,如果我這里有一些打印的語句或者線程睡眠的語句,子線程在 // 主線程將isRun改為false的時候,就會跳出死循環(huán),反之,如果循環(huán)體 // 內(nèi)是空的,就算在主線程改了isRun的值,也無法及時跳出循環(huán),why? // 當然,如果將isRun變量使用volatile修飾就沒有此問題 } } }).start(); } private void stop() { isRun = false; } }
有一點是一定的,就是子線程訪問isRun的時候會拷貝一份放到自己的線程(工作內(nèi)存)里,這樣在讀寫的時候可能就不會和外面isRun的值實時是匹配上的。所以就會出現(xiàn)意想不到的問題。
所以我們使用volatile修飾,這樣當有多線程同時訪問一個變量時,都會自動同步一下。顯然這樣會帶來一定的性能損失,但是如果確實需要還是要這么做的。
但是,有一個問題來了,使用volatile一定能就可解決多線程同步的問題了嗎?那我們看下面這個例子:
class TestSynchronize { // 使用volatile修飾的變量 private volatile int x = 0; private void add() { x++; } public void test() { // 啟動第一個線程,進行100萬次自加 new Thread(new Runnable() { @Override public void run() { for (int i=0; i< 1_000_000; i++) { add(); } System.out.println("第一個線程x=" + x); } }).start(); // 啟動第二個線程,進行100萬次自加 new Thread(new Runnable() { @Override public void run() { for (int i=0; i< 1_000_000; i++) { add(); } System.out.println("第二個線程x=" + x); } }).start(); } }
我們希望的結(jié)果是,最后一個執(zhí)行完的線程應(yīng)該是在2_000_000,但是只要你實際測下就發(fā)現(xiàn)并不是這樣,因為volatile只能保證可見性,但是只要涉及多線程我們一定還聽說過原子性這個概念。什么是可見性:
可見性:對于多個線程都在訪問的變量,當有個線程在修改的時候,它會保證會將修改的值更新到內(nèi)存中,而不是只在工作線程中修改,這樣當別的線程訪問的時候也會去內(nèi)存中取最新的值,這樣就能保證訪問到的值是最新的。
那什么又是原子性呢:
原子性:就是一個操作或者多個操作要么都執(zhí)行,要么都不執(zhí)行,不會存在執(zhí)行一半會被打斷。
在Java中,對基本數(shù)據(jù)類型變量的讀取和賦值操作是原子性的。但是上述代碼中的x++;
顯然不是原子操作,可以拆解為:
int temp = x + 1; x = temp;
那么這就為多線程操作帶來不確定性,
1、開始x初始值為0,
2、當線程A調(diào)用add()函數(shù)時,執(zhí)行到temp=x+1;
這一行時被中斷了,
3、此時切換到線程B的add()函數(shù),線程B完整執(zhí)行完兩行代碼后,x = 1了,
4、這個時候線程B又完整的執(zhí)行了一遍add方法,那么x=2了,
5、此時發(fā)生了線程切換,切換到A執(zhí)行,A接著上次的執(zhí)行的語句,temp = 1了,接下來執(zhí)行x = temp;
語句將1賦值給了x。
可是本來x都被B線程加到2了,這下又回去了,經(jīng)歷A和B線程一共三次add()操作,結(jié)果x的值只是1。
這就解釋了上面那段代碼中,兩個線程分別加了100萬次后,結(jié)果最后一個執(zhí)行完的線程打印的卻并不是200萬。原因就是add()里面的操作并不是原子性的,而volatile只能保證可見性,不能保證原子性
當然,僅針對上面的按理我們可以將int x = 0;
換一種類型聲明,比如使用AtomicInteger x = new AtomicInteger(0);
然后將x++
改成x.incrementAndGet();
這樣也能保證原子性,確保多線程操作后數(shù)據(jù)是符合期望的。
除了針對基本數(shù)據(jù)類型的,還有對引用操作原子化的,AtomicReference<V>
synchronized
當synchronized修飾一個方法時,那么同一時間只有一個線程可以訪問此方法,如果有多個方法都被synchronized修飾的話,當一個線程訪問了其中一個方法,別的線程就無法訪問其他被synchronized修飾的方法。
相當于有一個監(jiān)視器,當一個線程訪問某個方法,其他線程想訪問別的方法時,需要和同一個監(jiān)視器做確認,這么做看起來不太合理,其實也是合理的,比如有兩方法都可能對同一個變量做操作,兩個線程能同時訪問兩個方法,這樣數(shù)據(jù)還是會發(fā)生錯亂。
當然,我們就有兩個方法支持同步訪問的場景的,只要我們自己確認兩個方法不會存在數(shù)據(jù)上的錯亂,我們可以為每個方法指定自己的監(jiān)視器,在默認情況下是當前類的對象(this)。
我們分別為setName();
和其他兩個方法指定了不同的monitor(監(jiān)視器),這樣當線程A訪問上面兩個方法的時候,線程B想訪問方法setName也是不受影響的:
接下來我們看我們經(jīng)常寫的另一個例子,單例模式:
class TestInstance { private TestInstance(){} private static TestInstance sInstance; public static TestInstance newInstance() { **// ② 這里判空的目的?** if (sInstance == null) { **// ① 為什么鎖加在這里?** synchronized (TestInstance.class) { **// ③ 這里判空的目的?** if (sInstance == null) { sInstance = new TestInstance(); } } } return sInstance; } }
我們來依次搞清楚上面的三個問題,
①鎖為什么加在里面而不是在方法上加鎖,因為加鎖后會帶來性能上的損失的,單例對象只會創(chuàng)建一次,沒必要在實例已經(jīng)有的時候獲取單例時還加鎖,對性能是浪費。
②第一個判空的目的就是在已經(jīng)創(chuàng)建過實例之后的獲取操作,不用再經(jīng)過synchronized判斷,這樣更快。
③最后一個判空就是防止多個線程都會調(diào)到創(chuàng)建實例的操作。
到此這篇關(guān)于Java多線程之線程同步的文章就介紹到這了,更多相關(guān)Java線程同步內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot內(nèi)置的工具類之CollectionUtils示例講解
這篇文章主要介紹了Springboot內(nèi)置的工具類之CollectionUtils,本文通過示例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-12-12SpringBoot中基于JWT的單token授權(quán)和續(xù)期方案步驟詳解
在前后端分離架構(gòu)中,用戶登錄后,后端生成JWT?Token返給前端存于LocalStorage,每次請求攜帶Token驗證身份,后端校驗其有效性,本文給大家介紹SpringBoot中基于JWT的單token授權(quán)和續(xù)期方案步驟詳解,感興趣的朋友一起看看吧2024-09-09java數(shù)據(jù)結(jié)構(gòu)排序算法之樹形選擇排序詳解
這篇文章主要介紹了java數(shù)據(jù)結(jié)構(gòu)排序算法之樹形選擇排序,結(jié)合具體實例形式分析了java樹形選擇排序的原理、實現(xiàn)技巧與相關(guān)注意事項,需要的朋友可以參考下2017-05-05java實現(xiàn)word文檔轉(zhuǎn)pdf并添加水印的方法詳解
這篇文章主要介紹了java實現(xiàn)word文檔轉(zhuǎn)pdf并添加水印的方法,結(jié)合實例形式詳細分析了java word文檔轉(zhuǎn)PDF相關(guān)實現(xiàn)技巧與操作注意事項,需要的朋友可以參考下2019-09-09Eclipse中導入Maven Web項目并配置其在Tomcat中運行圖文詳解
這篇文章主要介紹了Eclipse中導入Maven Web項目并配置其在Tomcat中運行圖文詳解,需要的朋友可以參考下2017-12-12SpringBoot+RabbitMQ+Redis實現(xiàn)商品秒殺的示例代碼
本文主要介紹了SpringBoot+RabbitMQ+Redis實現(xiàn)商品秒殺,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11