假設(shè)一個(gè)這樣的場(chǎng)景: 在多線程的代碼中, 需要在不同的線程中對(duì)同一個(gè)變量進(jìn)行操作. 那此時(shí)就會(huì)出現(xiàn)問(wèn)題: 多線程是并發(fā)進(jìn)行的, 也就是說(shuō)代碼運(yùn)行的時(shí)候, 倆個(gè)線程會(huì)同時(shí)對(duì)一個(gè)變量進(jìn)行操作, 這樣就會(huì)涉及到多線程的安全問(wèn)題:
class Counter{
public int count;
public void add(){
count++;
}
}
public class demo1 {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.count);
}
}
在這個(gè)代碼中, 兩個(gè)線程會(huì)分別對(duì)count進(jìn)行自增五千次, 按理說(shuō)最后打印的結(jié)果是一萬(wàn). 但實(shí)際上,多次運(yùn)行后代碼的結(jié)果,很難做到一萬(wàn), 常見(jiàn)于八九千的結(jié)果.?
其原因在于, add的過(guò)程并非不可拆分的, 也就是不具有原子性. 在實(shí)際的運(yùn)行中, add可以大致分為三步: 讀取, 加一, 最后再賦值. 當(dāng)然這并非專業(yè)的術(shù)語(yǔ)說(shuō)法, 這里只簡(jiǎn)單的以此為描述.?
由于兩個(gè)線程同時(shí)進(jìn)行, 也就是都要執(zhí)行這三步, 且是以搶占式進(jìn)行執(zhí)行. 那執(zhí)行順序就必然亂套了. 很可能會(huì)出現(xiàn)線程1剛將count原值讀入, 線程2就將其賦值走了, 根本沒(méi)來(lái)得及加一. 這種還未執(zhí)行完就將其讀入的操作, 也可稱其為臟讀.?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
?為避免這種亂套的多線程安全問(wèn)題, 常用辦法便是采用加鎖(Synchronized), 其用于修飾方法和代碼塊. 但是特別注意, 加鎖是鎖的對(duì)象. 當(dāng)某個(gè)對(duì)象加鎖后, 只有當(dāng)其再解鎖后, 另一個(gè)線程才能重新獲取鎖, 否者會(huì)陷入阻塞等待的狀態(tài):
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
?這樣的操作就能保證在執(zhí)行完一整個(gè)add后再執(zhí)行下一個(gè)add. 雖會(huì)降低運(yùn)行速率, 但能保證代碼的準(zhǔn)確性. 代碼上的修改只需將add進(jìn)行加鎖即可保證得到準(zhǔn)確的結(jié)果:
//只需在此處加鎖即可
public synchronized void add(){
count++;
}
}
//或者代碼塊加鎖
public void add(){
synchronized (this) {
count++;
}
}
? 若兩個(gè)線程針對(duì)不同對(duì)象加鎖或者一個(gè)加鎖一個(gè)不加鎖, 那么也不會(huì)存在阻塞等待的情況.
還有一種特殊情況: 多重鎖. 即一個(gè)線程加了兩把鎖, 雖然說(shuō)當(dāng)一個(gè)線程對(duì)對(duì)象上鎖后, 另一個(gè)線程是應(yīng)該阻塞等待的, 但此時(shí)若上鎖線程就是要訪問(wèn)的線程呢? 這時(shí)是否可以考慮開(kāi)綠燈呢? 這就好比小偷偷不屬于自己的東西, 這是不被允許的犯罪行為. 那如果他偷的是自己的東西呢? 這完全是可以的, 因?yàn)檫@壓根就不算偷竊.
因此, 對(duì)于可以實(shí)現(xiàn)多重鎖的關(guān)鍵字, 就被認(rèn)為是可重入的, 反之是不可重入. 在java中的synchronized是屬于可重入, 也就是說(shuō), 加上述代碼合并運(yùn)行, 仍可以得到正確的結(jié)果, 但并非所有的鎖都支持該功能:
//可重入
public synchronized void add(){
synchronized (this) {
count++;
}
}
若不支持可重入, 則會(huì)陷入死鎖狀態(tài), 卡在那里 一直阻塞等待.
當(dāng)然, 死鎖的狀態(tài)并非只有上述的這一種. 第二種是兩個(gè)線程兩把鎖, 即兩個(gè)線程先分別加鎖, 然后再嘗試獲得對(duì)方的鎖:
public class demo2 {
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock2){
System.out.println("獲取鎖2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized ((lock1)){
System.out.println("獲取鎖1");
}
}
});
t1.start();
t2.start();
}
}
在這個(gè)代碼中就能夠看出, 當(dāng)兩個(gè)線程將鎖1 鎖2獲取后, 要相互獲取對(duì)方的鎖, 但對(duì)方的鎖未解鎖, 因此在這種情況想兩個(gè)線程都被阻塞, 不能繼續(xù)運(yùn)行. 在這種情況下代碼會(huì)一直處于運(yùn)行狀態(tài). 可以用jconsole觀察到線程是屬于阻塞狀態(tài).
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
?第三種死鎖即第二種死鎖的一般情況, 多線程多把鎖而非兩把鎖. 這里涉及到一個(gè)經(jīng)典的吃面問(wèn)題. 假設(shè), 有一個(gè)圓桌, 共坐了五個(gè)人, 每?jī)蓚€(gè)人之間, 放了一根筷子. 也就是說(shuō)共放了五根筷子.
假設(shè)吃面的人必須得先拿起他左邊的筷子, 再拿起他右邊的一根筷子. 那在這種情況下考慮極端情況, 當(dāng)五個(gè)人同時(shí)都想吃面時(shí), 會(huì)同時(shí)都拿起左邊的筷子, 且右邊沒(méi)有筷子可拿. 這個(gè)時(shí)候就僵住了, 誰(shuí)也吃不了面, 誰(shuí)也不會(huì)放下筷子. 同理, 在多線程種, 每個(gè)線程就好比每個(gè)人, 每跟筷子就好比每個(gè)鎖, 考慮極端情況, 會(huì)出現(xiàn)這種全部僵在一起的狀態(tài).
要解決這個(gè)問(wèn)題, 就得先了解死鎖的必要條件:\
1. 互斥使用. 線程一上鎖, 線程二只能等著.
2. 不可搶占. 線程一獲得鎖之后, 只能自己釋放鎖, 而不能由線程二強(qiáng)行獲取鎖
3.保持穩(wěn)定性. 若線程一已經(jīng)獲得鎖A, 它再嘗試獲得鎖B時(shí), 鎖A是不會(huì)因?yàn)榫€程一獲得鎖B而解鎖鎖A.
4.循環(huán)等待. 也就是剛才所演示的. 線程一獲得鎖A的同時(shí), 線程二獲得鎖B. 然后線程一要獲得鎖B, 線程二要獲得鎖A, 僵持不下.
對(duì)于Synchronized而言, 其實(shí)必要條件只有第四點(diǎn). 前三點(diǎn)是無(wú)法去改變的. 但對(duì)于其他鎖來(lái)說(shuō)不一定. 因此, 想要解決死鎖, 就只能從, 循環(huán)等待入手.
解決方法是, 給每一把鎖標(biāo)號(hào), 再按照標(biāo)號(hào)的一定順序進(jìn)行加鎖.
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
以吃面來(lái)舉例. 將每根筷子標(biāo)號(hào), 并規(guī)定拿筷子必須從小號(hào)開(kāi)始拿. 對(duì)應(yīng)多線程種按鎖的標(biāo)號(hào)順序由小到大加鎖. 這樣的話, 一號(hào)筷子和二號(hào)筷子之間的人就拿一號(hào), 二號(hào)筷子和三號(hào)筷子之間的人就拿二號(hào), 以此類推.
當(dāng)輪到一號(hào)筷子和五號(hào)筷子之間的人拿筷子時(shí), 出現(xiàn)問(wèn)題了. 由于規(guī)定按小號(hào)拿, 因此應(yīng)該是拿一號(hào)筷子而非五號(hào)筷子. 但此時(shí)的一號(hào)筷子已經(jīng)被占用. 因此他只能等待, 也就是多線程中的阻塞. 與此同時(shí), 前一個(gè)人可以再拿到四號(hào)筷子的基礎(chǔ)上拿到五號(hào)筷子, 也就是獲取到鎖, 從而執(zhí)行多線程. 以這種方式, 就不會(huì)出現(xiàn)所有人都吃不到面, 避免所有線程都處于阻塞狀態(tài). 反應(yīng)到代碼中, 就只需將鎖調(diào)換一下即可:
public class demo2 {
public static void main(String[] args) {
Object lock1 = new Object();
Object lock2 = new Object();
//標(biāo)號(hào): 鎖1 為一號(hào), 鎖2 為二號(hào). 由小到大加鎖
Thread t1 = new Thread(() -> {
synchronized (lock1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (lock2){
System.out.println("獲取鎖2");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized ((lock2)){
System.out.println("獲取鎖1");
}
}
});
t1.start();
t2.start();
}
}
除此以外, 解決這類問(wèn)題還可以使用銀行家算法. 但是在實(shí)際工作中, 使用并不廣泛. 因?yàn)槠溥^(guò)于復(fù)雜, 實(shí)用性不高.文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-468400.html
-------------------------------------------最后編輯于2023.6.1 下午兩點(diǎn)左右文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-468400.html
到了這里,關(guān)于編程(39)----------多線程中的鎖的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!