Java多線程之線程同步
volatile
先看個(gè)例子
class Test {
// 定義一個(gè)全局變量
private boolean isRun = true;
// 從主線程調(diào)用發(fā)起
public void process() {
test();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
stop();
}
// 啟動(dòng)一個(gè)子線程循環(huán)讀取isRun
private void test() {
new Thread(new Runnable() {
@Override
public void run() {
while (isRun) {
// 疑問(wèn),如果我這里有一些打印的語(yǔ)句或者線程睡眠的語(yǔ)句,子線程在
// 主線程將isRun改為false的時(shí)候,就會(huì)跳出死循環(huán),反之,如果循環(huán)體
// 內(nèi)是空的,就算在主線程改了isRun的值,也無(wú)法及時(shí)跳出循環(huán),why?
// 當(dāng)然,如果將isRun變量使用volatile修飾就沒有此問(wèn)題
}
}
}).start();
}
private void stop() {
isRun = false;
}
}
有一點(diǎn)是一定的,就是子線程訪問(wèn)isRun的時(shí)候會(huì)拷貝一份放到自己的線程(工作內(nèi)存)里,這樣在讀寫的時(shí)候可能就不會(huì)和外面isRun的值實(shí)時(shí)是匹配上的。所以就會(huì)出現(xiàn)意想不到的問(wèn)題。
所以我們使用volatile修飾,這樣當(dāng)有多線程同時(shí)訪問(wèn)一個(gè)變量時(shí),都會(huì)自動(dòng)同步一下。顯然這樣會(huì)帶來(lái)一定的性能損失,但是如果確實(shí)需要還是要這么做的。
但是,有一個(gè)問(wèn)題來(lái)了,使用volatile一定能就可解決多線程同步的問(wèn)題了嗎?那我們看下面這個(gè)例子:
class TestSynchronize {
// 使用volatile修飾的變量
private volatile int x = 0;
private void add() {
x++;
}
public void test() {
// 啟動(dòng)第一個(gè)線程,進(jìn)行100萬(wàn)次自加
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i< 1_000_000; i++) {
add();
}
System.out.println("第一個(gè)線程x=" + x);
}
}).start();
// 啟動(dòng)第二個(gè)線程,進(jìn)行100萬(wàn)次自加
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i< 1_000_000; i++) {
add();
}
System.out.println("第二個(gè)線程x=" + x);
}
}).start();
}
}
我們希望的結(jié)果是,最后一個(gè)執(zhí)行完的線程應(yīng)該是在2_000_000,但是只要你實(shí)際測(cè)下就發(fā)現(xiàn)并不是這樣,因?yàn)関olatile只能保證可見性,但是只要涉及多線程我們一定還聽說(shuō)過(guò)原子性這個(gè)概念。什么是可見性:
可見性:對(duì)于多個(gè)線程都在訪問(wèn)的變量,當(dāng)有個(gè)線程在修改的時(shí)候,它會(huì)保證會(huì)將修改的值更新到內(nèi)存中,而不是只在工作線程中修改,這樣當(dāng)別的線程訪問(wèn)的時(shí)候也會(huì)去內(nèi)存中取最新的值,這樣就能保證訪問(wèn)到的值是最新的。
那什么又是原子性呢:
原子性:就是一個(gè)操作或者多個(gè)操作要么都執(zhí)行,要么都不執(zhí)行,不會(huì)存在執(zhí)行一半會(huì)被打斷。
在Java中,對(duì)基本數(shù)據(jù)類型變量的讀取和賦值操作是原子性的。但是上述代碼中的x++;顯然不是原子操作,可以拆解為:
int temp = x + 1; x = temp;
那么這就為多線程操作帶來(lái)不確定性,
1、開始x初始值為0,
2、當(dāng)線程A調(diào)用add()函數(shù)時(shí),執(zhí)行到temp=x+1;這一行時(shí)被中斷了,
3、此時(shí)切換到線程B的add()函數(shù),線程B完整執(zhí)行完兩行代碼后,x = 1了,
4、這個(gè)時(shí)候線程B又完整的執(zhí)行了一遍add方法,那么x=2了,
5、此時(shí)發(fā)生了線程切換,切換到A執(zhí)行,A接著上次的執(zhí)行的語(yǔ)句,temp = 1了,接下來(lái)執(zhí)行x = temp;語(yǔ)句將1賦值給了x。
可是本來(lái)x都被B線程加到2了,這下又回去了,經(jīng)歷A和B線程一共三次add()操作,結(jié)果x的值只是1。
這就解釋了上面那段代碼中,兩個(gè)線程分別加了100萬(wàn)次后,結(jié)果最后一個(gè)執(zhí)行完的線程打印的卻并不是200萬(wàn)。原因就是add()里面的操作并不是原子性的,而volatile只能保證可見性,不能保證原子性
當(dāng)然,僅針對(duì)上面的按理我們可以將int x = 0;換一種類型聲明,比如使用AtomicInteger x = new AtomicInteger(0);然后將x++改成x.incrementAndGet();這樣也能保證原子性,確保多線程操作后數(shù)據(jù)是符合期望的。
除了針對(duì)基本數(shù)據(jù)類型的,還有對(duì)引用操作原子化的,AtomicReference<V>
synchronized
當(dāng)synchronized修飾一個(gè)方法時(shí),那么同一時(shí)間只有一個(gè)線程可以訪問(wèn)此方法,如果有多個(gè)方法都被synchronized修飾的話,當(dāng)一個(gè)線程訪問(wèn)了其中一個(gè)方法,別的線程就無(wú)法訪問(wèn)其他被synchronized修飾的方法。

相當(dāng)于有一個(gè)監(jiān)視器,當(dāng)一個(gè)線程訪問(wèn)某個(gè)方法,其他線程想訪問(wèn)別的方法時(shí),需要和同一個(gè)監(jiān)視器做確認(rèn),這么做看起來(lái)不太合理,其實(shí)也是合理的,比如有兩方法都可能對(duì)同一個(gè)變量做操作,兩個(gè)線程能同時(shí)訪問(wèn)兩個(gè)方法,這樣數(shù)據(jù)還是會(huì)發(fā)生錯(cuò)亂。
當(dāng)然,我們就有兩個(gè)方法支持同步訪問(wèn)的場(chǎng)景的,只要我們自己確認(rèn)兩個(gè)方法不會(huì)存在數(shù)據(jù)上的錯(cuò)亂,我們可以為每個(gè)方法指定自己的監(jiān)視器,在默認(rèn)情況下是當(dāng)前類的對(duì)象(this)。

我們分別為setName();和其他兩個(gè)方法指定了不同的monitor(監(jiān)視器),這樣當(dāng)線程A訪問(wèn)上面兩個(gè)方法的時(shí)候,線程B想訪問(wèn)方法setName也是不受影響的:

接下來(lái)我們看我們經(jīng)常寫的另一個(gè)例子,單例模式:
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;
}
}
我們來(lái)依次搞清楚上面的三個(gè)問(wèn)題,
①鎖為什么加在里面而不是在方法上加鎖,因?yàn)榧渔i后會(huì)帶來(lái)性能上的損失的,單例對(duì)象只會(huì)創(chuàng)建一次,沒必要在實(shí)例已經(jīng)有的時(shí)候獲取單例時(shí)還加鎖,對(duì)性能是浪費(fèi)。
②第一個(gè)判空的目的就是在已經(jīng)創(chuàng)建過(guò)實(shí)例之后的獲取操作,不用再經(jīng)過(guò)synchronized判斷,這樣更快。
③最后一個(gè)判空就是防止多個(gè)線程都會(huì)調(diào)到創(chuàng)建實(shí)例的操作。
到此這篇關(guān)于Java多線程之線程同步的文章就介紹到這了,更多相關(guān)Java線程同步內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Springboot內(nèi)置的工具類之CollectionUtils示例講解
這篇文章主要介紹了Springboot內(nèi)置的工具類之CollectionUtils,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12
SpringBoot中基于JWT的單token授權(quán)和續(xù)期方案步驟詳解
在前后端分離架構(gòu)中,用戶登錄后,后端生成JWT?Token返給前端存于LocalStorage,每次請(qǐng)求攜帶Token驗(yàn)證身份,后端校驗(yàn)其有效性,本文給大家介紹SpringBoot中基于JWT的單token授權(quán)和續(xù)期方案步驟詳解,感興趣的朋友一起看看吧2024-09-09
使用Mybatis的PageHelper分頁(yè)工具的教程詳解
這篇文章主要介紹了使用Mybatis的PageHelper分頁(yè)工具的教程,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09
java數(shù)據(jù)結(jié)構(gòu)排序算法之樹形選擇排序詳解
這篇文章主要介紹了java數(shù)據(jù)結(jié)構(gòu)排序算法之樹形選擇排序,結(jié)合具體實(shí)例形式分析了java樹形選擇排序的原理、實(shí)現(xiàn)技巧與相關(guān)注意事項(xiàng),需要的朋友可以參考下2017-05-05
Spring Boot將項(xiàng)目打包成war包的操作方法
這篇文章主要介紹了Spring Boot將項(xiàng)目打包成war包的操作方法,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-09-09
java實(shí)現(xiàn)word文檔轉(zhuǎn)pdf并添加水印的方法詳解
這篇文章主要介紹了java實(shí)現(xiàn)word文檔轉(zhuǎn)pdf并添加水印的方法,結(jié)合實(shí)例形式詳細(xì)分析了java word文檔轉(zhuǎn)PDF相關(guān)實(shí)現(xiàn)技巧與操作注意事項(xiàng),需要的朋友可以參考下2019-09-09
Java實(shí)現(xiàn)Huffman編碼的示例代碼
Huffman編碼是一種編碼方式,本文主要介紹了Java實(shí)現(xiàn)Huffman編碼的示例代碼,具有一定的參考價(jià)值,感興趣的可以了解一下2023-08-08
Eclipse中導(dǎo)入Maven Web項(xiàng)目并配置其在Tomcat中運(yùn)行圖文詳解
這篇文章主要介紹了Eclipse中導(dǎo)入Maven Web項(xiàng)目并配置其在Tomcat中運(yùn)行圖文詳解,需要的朋友可以參考下2017-12-12
SpringBoot+RabbitMQ+Redis實(shí)現(xiàn)商品秒殺的示例代碼
本文主要介紹了SpringBoot+RabbitMQ+Redis實(shí)現(xiàn)商品秒殺,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11

