Java執行緒的同步與死鎖
導語
本篇內容瞭解就OK了。重點有一個關鍵字synchronized。
主要內容
- 執行緒同步產生原因
- 執行緒的同步處理操作
- 執行緒的死鎖情況
具體內容
同步問題的引出
實際上所謂的同步指的就是多個執行緒訪問同一資源時所需要考慮到的問題。
範例:觀察非同步情況下的操作
class MyThread implements Runnable {
private int ticket = 5; // 一共有5張票
public void run() {
for(int i = 0; i < 20; i++) {
if (this.ticket > 0) {
System.out.println(Thread.currentThread().getName() + " 賣票,ticket = " + this.ticket--);
}
}
}
}
public class TestDemo {
public static void main(String args[]) {
MyThread mt = new MyThread();
new Thread(mt, "票販子A").start();
new Thread(mt, "票販子B").start();
new Thread(mt, "票販子C").start();
new Thread(mt, "票販子D").start();
}
}
現在四個執行緒物件應該一起銷售出5張票。
輸出結果(情況隨機)
票販子D 賣票,ticket = 5
票販子A 賣票,ticket = 2
票販子B 賣票,ticket = 4
票販子C 賣票,ticket = 3
票販子D 賣票,ticket = 1
此時沒有出現問題是因為是在一個JVM程序下執行,並且沒有受到任何的影響,如果想要觀察到問題,可以加入一個延遲。
修改程式碼
class MyThread implements Runnable {
private int ticket = 5; // 一共有5張票
public void run() {
for(int i = 0; i < 20; i++) {
if(this.ticket > 0) {
try {
Thread.sleep(100);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 賣票,ticket = " + this.ticket--);
}
}
}
}
輸出結果(情況隨機)
票販子B 賣票,ticket = 4
票販子A 賣票,ticket = 3
票販子C 賣票,ticket = 5
票販子D 賣票,ticket = 2
票販子A 賣票,ticket = 1
票販子B 賣票,ticket = 0
票販子C 賣票,ticket = -1
票販子D 賣票,ticket = -2
此時執行之後發現操作的結果出現了負數,那就是不同步的情況,到底是如何造成不同步呢?
整個賣票分為兩步:
- 第一步:判斷是否有剩餘票。
- 第二步:修改剩餘票數。
當票還有一張的時候,第一個執行緒進入判斷語句,開始休眠,另一個執行緒也進入判斷語句內,100毫秒後,票數都減1,所以就出現了負數。
這種情況屬於非同步操作,非同步操作就容易出現安全隱患。
同步操作
通過觀察可以發現以上程式所帶來的最大問題在:判斷和修改資料是分開完成的,即,某幾個執行緒可以同時執行這些功能。
如果想解決這樣的問題,就必須使用同步,所謂的同步就是指多個操作在同一個時間段內只能有一個執行緒進行,其它執行緒要等待此執行緒完成之後才可以繼續執行。
在Java裡面如果想要實現執行緒的同步可以使用synchronized關鍵字。而這個關鍵字可以通過兩種方式使用:
- 同步程式碼塊。
- 同步方法。
範例:觀察同步塊解決
class MyThread implements Runnable {
private int ticket = 50;
public void run() {
for(int i = 0; i < 200; i++) {
synchronized(this) { // 當前操作每次只允許一個物件進入
if(this.ticket > 0) {
try {
Thread.sleep(100);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 賣票,ticket = " + this.ticket--);
}
}
}
}
}
輸出比較長,發現可以解決問題,但是這樣的寫法比較麻煩,我們可以使用同步方法解決。
範例:同步方法解決
class MyThread implements Runnable {
private int ticket = 50;
public void run() {
for(int i = 0; i < 200; i++) {
this.sale(); // 呼叫同步方法
}
}
public synchronized void sale() { // 同步方法
if(this.ticket > 0) {
try {
Thread.sleep(100);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 賣票,ticket = " + this.ticket--);
}
}
}
輸出結果和之前沒有區別。
同步操作與非同步操作相比,非同步操作的執行速度要高於同步操作,但是同步操作時資料的安全性越高,屬於安全的執行緒操作。
死鎖
實際上通過發現,所謂的同步指的就是一個執行緒物件等待另外一個執行緒物件執行完畢後的操作形式。執行緒同步過多就有可能造成死鎖。
下面編寫一個程式,本程式無實際意義,觀察死鎖的情況。
範例:觀察死鎖
class A {
public synchronized void say(B b) {
System.out.println("我說:把你的本給我,我給你筆,否則不給!");
b.get();
}
public synchronized void get() {
System.out.println("我:得到了本,付出了筆,還是什麼都寫不了!");
}
}
class B {
public synchronized void say(A a) {
System.out.println("你說:把你的筆給我,我給你本,否則不給!");
a.get();
}
public synchronized void get() {
System.out.println("你:得到了筆,付出了本,還是什麼都寫不了!");
}
}
public class TestDemo implements Runnalbe {
private static A a = new A();
private static B b = new B();
public static void main(String args[]) {
new TestDemo();
}
public TestDemo() {
new Thread(this).start();
b.say(a);
}
public void run() {
b.say(b);
}
}
輸出結果
你說:把你的筆給我,我給你本,否則不給!
我說:把你的本給我,我給你筆,否則不給!
然後一直在執行,是因為死鎖,兩個執行緒都在等待。
死鎖是程式開發之中,由於某種邏輯上的錯誤所造成的問題,並且不是簡單的就會出現的。
面試題:請解釋多個緩和訪問一同資源時需要考慮到哪些情況?有可能帶來哪些問題?
- 多個執行緒訪問同一資源時一定要處理好同步,可以使用同步程式碼塊或同步方法來解決。
- 同步程式碼塊:synchronized(鎖定物件) {程式碼}。
- 同步方法:public synchronized 返回值 方法名稱() {程式碼}。
- 但是過多的使用同步,有可能造成死鎖。
總結
- 簡單理解同步和非同步操作可以通過synchronized來實現。
- 死鎖是一種不定的狀態。