說到多線程編程,一定少不了線程安全這個話題。我們前面了解了線程的原理以及線程與進程的關系。線程之間共享資源,這就代表了在多線程編程中一定會產生沖突,所以我們需要在敲代碼時保證線程安全,避免這樣的問題發生。
我們先看一個代碼案例
public class Test10 {static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t2.start();//保證線程都執行完Thread.sleep(1000);System.out.println(count);}
}
在我們看來,運行的結果應該是100000,但是事實并非如此。
運行結果:
1.隨機調度
count++;
這段代碼是看似是一個指令,但實際上是分步執行的。
分為三步的:
1.讀取數據
2.修改數據
3.放回內存
在執行過程中,CPU資源是隨時會被調度走的,也就是說,如果執行到了讀取內存,有可能會被立刻調度走的。這就是所謂的隨機調度。
為了解釋上面的案例可以畫一個時間線:
這也就是造成上面現象的原因之一。
那我們應該如何解決呢?
解決方案1:
利用join方法,在t1執行完后再執行t2,這樣雖然能解決問題,但是失去了并發執行的意義,串行執行的效率也比較低。
public class Test10 {static int count = 0;public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {count++;}});t1.start();t1.join();t2.start();t2.join();//保證線程都執行完Thread.sleep(1000);System.out.println(count);}
}
2.鎖(synchronized)
對于解決上述的線程問題,引入了鎖這一概念。
鎖是什么,我們可以舉個具體的例子來了解一下:
我們可以把鎖看成家里的鎖,當家里沒人時,門是從外面鎖上的,這時擁有鑰匙的人就可以打開鎖并進去執行任務,但為了在執行任務的時候是安全的,所以會從里面鎖上,這時外面的人就算有鑰匙也打不開門,當里面的人執行完任務的出來后還會把門帶上,這時外面擁有鑰匙的人就可以開門進去了。這其實就是鎖是如何解決線程安全問題的類似原理。
public class Test10 {static int count = 0;public static void main(String[] args) throws InterruptedException {Object locker = new Object();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (locker) {count++;}}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {synchronized (locker) {count++;}}});t1.start();t2.start();// t1.start();
// t1.join();
// t2.start();
// t2.join();//保證線程都執行完Thread.sleep(1000);System.out.println(count);}
}
運行結果:
我們先了解一下使用鎖的格式:
synchronized(鎖對象){}
這里的鎖對象可以是任意引用類型的對象,但要保證在解決同一個非原子問題時是同一個對象。
synchronized的使用方法除了上述格式以外,還能用來修飾方法:
這里的鎖對象是this。
class Counter{int count = 0;public synchronized void addCount(){count++;}
}public class Test11 {public static void main(String[] args) throws InterruptedException {Object locker = new Object();Counter counter = new Counter();Thread t1 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.addCount();}});Thread t2 = new Thread(()->{for (int i = 0; i < 50000; i++) {counter.addCount();}});t1.start();t2.start();Thread.sleep(100);System.out.println(counter.count);}
}
運行結果:
其實這樣的解決方法也就是將局部的任務給串行化,只不過比直接將整個線程串行化來的含蓄,性能降低的少。
可重入
在Java中synchronized具有可重入的特點。
synchronized (locker) {synchronized (locker) {count++;}}
我們知道,鎖是具有互斥性的,也就是在上鎖后是需要解鎖后才能讓下一個擁有鎖對象的任務執行,那上面這段代碼就會形成一個死鎖的現象,當進入第一個鎖后,會遇到第二個鎖,但是想要進入第二個synchronized是需要從第一個synchronized中出來的,但是要想從第一個synchronized中出來就需要進入第二個synchronized,所以這就形成了一個死循環,可以叫死鎖。
Java的開發人員為了解決這一問題就賦予了synchronized可重入這一屬性,也就是在上述情況下不會出現死鎖的現象,Java會自動識別出來。
如果有很多層synchronized嵌套的話,當第一次進入的synchronized結束時,這把鎖才會解開。
死鎖?
上面講可重入性的時候講到了死鎖這一概念,這里就詳細講講死鎖。
造成死鎖有三種情況:
1.一個線程同一個鎖加多次,這也是講述可重入性時舉的例子。
2.N個線程,M把鎖。
public class Test12 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){try {System.out.println("t1獲取了locker1!");//確保t2先拿到locker2Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1獲取了兩把鎖!");}}});Thread t2 = new Thread(()->{synchronized (locker2){try {System.out.println("t2獲取了locker2!");//確保線程t1先拿到locker1Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){System.out.println("t2獲取了兩把鎖!");}}});t1.start();t2.start();}
}
運行結果:
進程并沒有結束,使用jconsole查看線程狀態,發現是BLOCKED,也就鎖造成的無上限的阻塞等待。這是因為在線程t1拿到locker1和t2拿到locker2的情況下,t1要想拿到locker2就必須要讓t2解鎖,而t2要想解鎖,就需要拿到locker1,但是locker1在t1手中,所以就形成了死循環,也就構成了死鎖。
3.哲學家就餐問題
假設有七個哲學家圍著一個桌子吃飯,每兩個人中間放一根筷子,這樣的話當一個人用筷子吃飯的時候,他兩側的人都沒法用餐。
在一種極端情況下,每個哲學家都拿起他左手邊的筷子,這樣所有哲學家都在等待對方放下筷子,就形成了死鎖。
public class Test13 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Object locker3 = new Object();Object locker4 = new Object();Object locker5 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker5){}}});Thread t2 = new Thread(()->{synchronized (locker2){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){}}});Thread t3 = new Thread(()->{synchronized (locker3){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){}}});Thread t4 = new Thread(()->{synchronized (locker4){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker3){}}});Thread t5 = new Thread(()->{synchronized (locker5){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker4){}}});t1.start();t2.start();t3.start();t4.start();t5.start();}
}
各線程狀態:?
解決方案:
1.對筷子按順序進行編號,先拿到左右小的編號的筷子,拿到后再拿左右大的編號的筷子。
剛開始1號先吃到飯,吃完后放下1號和7號筷子,2號拿到1號筷子,7號拿到7號筷子,7號可以就餐,以此類推,所有人都可以完成就餐。
public class Test13 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Object locker3 = new Object();Object locker4 = new Object();Object locker5 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker5){}}});Thread t2 = new Thread(()->{//保證t1先拿到locker1,如果t2先拿到locker1,還是會形成死鎖try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker1){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker2){}}});Thread t3 = new Thread(()->{synchronized (locker2){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker3){}}});Thread t4 = new Thread(()->{synchronized (locker3){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker4){}}});Thread t5 = new Thread(()->{synchronized (locker4){try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (locker5){}}});t1.start();t2.start();t3.start();t4.start();t5.start();}
}
運行結果:
?4.死鎖總結:
造成死鎖的原因:
1.鎖具有互斥性(鎖的基本特性)
當一個鎖被一個線程獲取之后,當別的線程想要獲取這個鎖的時候,會線程阻塞。
2.鎖不可搶占(鎖的基本特性)
當這個鎖已經被獲取時,別的線程是不能強行搶占這個鎖的,?必須等待獲取。
3.請求和保持
當一個線程已經有至少一個鎖的時候,嘗試獲取別的鎖遇到阻塞,這時候該線程也不會放棄原來的鎖。
4.循環等待
線程1等待線程2,線程2等待線程3,線程3等待線程4,線程4等待線程5,線程5等待線程1,這樣就產生了死循環。
解決方案:
1.把嵌套的鎖改為并列的鎖。(基于N個線程,M把鎖的代碼例子)
public class Test14 {public static void main(String[] args) {Object locker1 = new Object();Object locker2 = new Object();Thread t1 = new Thread(()->{synchronized (locker1){System.out.println("t1拿到locker1!");
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }}synchronized (locker2){System.out.println("t1拿到locker2!");}});Thread t2 = new Thread(()->{synchronized (locker2){System.out.println("t2拿到locker2!");
// try {
// Thread.sleep(100);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }}synchronized (locker1){System.out.println("t2拿到locker1!");}});t1.start();t2.start();}
}
運行結果:
2.規定加鎖順序編號遞增/遞減(基于哲學家就餐問題)
代碼案例在上述哲學家就餐問題中的Test13.
3.java標準庫中的線程安全類:
StringBuffer,Hashtable,Vector,ConcurrentHashMap,String。。。。。。
前三個不推薦使用。
不安全類:
StringBuilder,ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet。
以StringBuffer為例,查看標準庫中的代碼,發現其內部是由簡單加synchronized實現的,當面對比較復雜的情況時,很有可能會出現bug~~
4.wait/notify
在有些情況下我們需要讓某個線程處于阻塞狀態,在完成某些任務后再進行喚醒。
注意在調用wait/notify時必須實在synchronized的代碼塊中,并且必須是相同的鎖對象才行。
wait下的線程狀態:WAITING
public class Test16 {public static void main(String[] args) {Object locker = new Object();Thread t1 = new Thread(()->{synchronized (locker) {System.out.println("線程t1wait......");try {locker.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("線程t1被喚醒!");}});Thread t2 = new Thread(()->{synchronized (locker) {try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("t2線程嘗試喚醒t1......");locker.notify();}});t1.start();t2.start();}
}
運行結果:
、
在線程wait期間,該線程是主動放棄了CPU資源的,是解鎖狀態,暫時不會參與鎖競爭。
這種情況下,當使用notify喚醒時只能喚醒其中一個,并且是隨機的,這就有很大的不確定性在里面,所以java標準庫中還提供了notifyAll方法,能夠喚醒所有相同鎖對象的wait。
對于notify是隨即喚醒這一點還有可能會造成線程餓死,所謂線程餓死也就是某個線程長時間沒有吃到CPU的資源。
public class Test17 {public static void main(String[] args) {Object l1 = new Object();Thread t1 = new Thread(()->{synchronized (l1){try {l1.wait();System.out.println("t1被喚醒!");} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t2 = new Thread(()->{synchronized (l1){try {l1.wait();System.out.println("t2被喚醒!");} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t3 = new Thread(()->{synchronized (l1){try {l1.wait();System.out.println("t3被喚醒!");} catch (InterruptedException e) {throw new RuntimeException(e);}}});Thread t4 = new Thread(()->{System.out.println("輸入任意內容喚醒所有線程:");Scanner sc = new Scanner(System.in);sc.next();synchronized (l1){l1.notifyAll();}});t1.start();t2.start();t3.start();t4.start();}
}
運行結果:
?