Thread - 同步
問題引出
我們現在來通過Runnable接口實現多線程,產生3個線程對象,模擬賣票的場景!
class MyThread implements Runnable {
private int ticket = 5;
public void run() {
for (int i = 0; i < 100; i++) {
if (ticket > 0) {
try {
Thread.sleep(300); // 加入延遲
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("賣票:ticket = " + ticket--);
}
}
}
};
public class SyncDemo01 {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
Thread t3 = new Thread(mt);
t1.start();
t2.start();
t3.start();
}
}
我們執行下這段代碼,結果如下:
賣票:ticket = 5
賣票:ticket = 4
賣票:ticket = 5
賣票:ticket = 3
賣票:ticket = 2
賣票:ticket = 2
賣票:ticket = 1
賣票:ticket = 0
賣票:ticket = -1 // 票數竟然還能負數?
爲什麼會出現“負數”的情況:在上面的操作中,我們可以發現,因爲加入了“延遲操作”一個線程很有可能在還沒對票數進行減操作之前,其他線程就已經將票數減少了,這樣就會出現票數爲負的情況。
有沒有方法解決?肯定是有的!想解決這樣的問題,就必須使用同步!所謂同步,就是指多個操作在同一個時間段內只能有一個線程進行,其他線程要等待此線程完成之後纔可以繼續執行。
解決問題
解決資源共享的同步操作,有兩種方法:同步代碼塊 和 同步方法 !
同步代碼塊
所謂代碼塊就是指使用“{}”括起來的一段代碼,如果在代碼塊上加上synchronized關鍵字,則此代碼塊就成爲同步代碼塊。
【同步代碼塊 - 格式】
synchronized(同步對象) {
需要同步的代碼 ;
}
我們對代碼進行修改:
class MyThread implements Runnable {
private int ticket = 5;
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (this) { // 加入同步操作
if (ticket > 0) {
try {
Thread.sleep(300); // 加入延遲
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("賣票:ticket = " + ticket--);
}
}
}
}
};
public class SyncDemo01 {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
Thread t3 = new Thread(mt);
t1.start();
t2.start();
t3.start();
}
}
我們重新執行下這段代碼,結果如下:
賣票:ticket = 5
賣票:ticket = 4
賣票:ticket = 3
賣票:ticket = 2
賣票:ticket = 1
同步方法
除了可以將需要的代碼設置成同步代碼塊外,也可以使用synchronized關鍵字將一個方法聲明成同步方法。
【同步方法 - 格式】
synchronized 方法返回值 方法名稱(參數列表) {
}
我們採用同步方法對代碼進行修改:
class MyThread implements Runnable {
private int ticket = 5;
public void run() {
for (int i = 0; i < 100; i++) {
this.sale(); // 調用同步方法
}
}
public synchronized void sale() { // 聲明同步方法
if (ticket > 0) {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("賣票:ticket = " + ticket--);
}
}
};
public class SyncDemo01 {
public static void main(String[] args) {
MyThread mt = new MyThread();
Thread t1 = new Thread(mt);
Thread t2 = new Thread(mt);
Thread t3 = new Thread(mt);
t1.start();
t2.start();
t3.start();
}
}
我們重新執行下這段代碼,結果如下:
賣票:ticket = 5
賣票:ticket = 4
賣票:ticket = 3
賣票:ticket = 2
賣票:ticket = 1
從以上程序的運行結果可以發現,此代碼完成了與之前同步代碼塊同樣的功能。
總結
多個線程共享同一資源時需要進行同步,以保證資源操作的完整性。
Thread - 死鎖
通過上面的例子,我們發現,同步還是很有好處的,它可以保證資源共享操作的正確性,但是過多的同步也會產生問題,這就是我們接下來要討論“死鎖”問題!
什麼是死鎖?
多線程以及多進程改善了系統資源的利用率並提高了系統 的處理能力。然而,併發執行也帶來了新的問題 -- 死鎖。
在編寫多線程的時候,必須要注意資源的使用問題,如果兩個或多個線程分別擁有不同的資源,而同時又需要對方釋放資源才能繼續運行時,就會發生死鎖。
簡單來說:死鎖就是當一個或多個進程都在等待系統資源,而資源本身又被佔用時,所產生的一種狀態。
造成死鎖的原因
多個線程競爭共享資源,由於資源被佔用,資源不足或進程推進順序不當等原因造成線程處於永久阻塞狀態,從而引發死鎖。
造成死鎖的四個條件
1、互斥條件:進程對於所分配到的資源具有排它性,即一個資源只能被一個進程佔用,直到被該進程釋放
2、請求和保持條件:一個進程因請求被佔用資源而發生阻塞時,對已獲得的資源保持不放。
3、不剝奪條件:任何一個資源在沒被該進程釋放之前,任何其他進程都無法對他剝奪佔用
4、循環等待條件:當發生死鎖時,所等待的進程必定會形成一個環路(類似於死循環),造成永久阻塞。
問題引出
現在張三想要李四的畫,李四想要張三的書,於是產生了以下對話:
張三對李四說:“把你的畫給我,我就給你書”
李四對張三說:“把你的書給我,我就給你畫”
此時,張山在等着李四的答覆,李四也在等着張三的答覆,那麼這樣下去的結果就是,兩個人都在等待,但是都沒有結果,這就是“死鎖”!
從線程角度來說,所謂死鎖就是指兩個線程都在等待彼此先完成,造成了程序的停滯,一般程序的死鎖都是在程序運行時出現,比如我們通過一個代碼範例來看看發生死鎖的場景。
class Zhangsan {
public void say() {
System.out.println("Zhangsan say: give me your painting, i will give you my book!");
}
public void get() {
System.out.println("Zhangsan got Lisi's painting!");
}
}
class Lisi {
public void say() {
System.out.println("Lisi say: give me your book, i will give you my painting!");
}
public void get() {
System.out.println("Lisi got Zhangsan's book!");
}
}
public class ThreadDeadLock implements Runnable {
private static Zhangsan zs = new Zhangsan(); // 實例化static型對象,數據共享
private static Lisi ls = new Lisi(); // 實例化static型對象,數據共享
private boolean flag = false; // 聲明標記,用於判斷哪個對象先執行
public void run() {
if (flag) { // 判斷標誌位,flag爲true,Zhangsan先執行
synchronized (zs) { // 同步第一個對象
zs.say();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (ls) { // 同步第二個對象
zs.get();
}
}
} else { // Lisi先執行
synchronized (ls) { // 同步第二個對象
ls.say();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (zs) { // 同步第一個對象
ls.get();
}
}
}
}
public static void main(String[] args) {
ThreadDeadLock t1 = new ThreadDeadLock();
ThreadDeadLock t2 = new ThreadDeadLock();
t1.flag = true;
t2.flag = false;
Thread thA = new Thread(t1);
Thread thB = new Thread(t2);
thA.start();
thB.start();
}
}
我們執行下這段代碼,結果如下:
Zhangsan say: give me your painting, i will give you my book!
Lisi say: give me your book, i will give you my painting!
從程序的運行結果中可以看出,兩個線程都在彼此等待着對方的執行完成,這樣,程序就無法向下繼續執行,從而造成了死鎖的現象。
解決問題
要預防和避免死鎖的發生,只需將上面所講到的4個條件破壞掉其中之一即可。
如上面的代碼當中,有四個同步代碼塊,只需要將其中一個同步代碼塊去掉,即可解決死鎖問題,一般而言破壞“循環等待”這個條件是解決死鎖最有效的方法。