昨天重新學習了java多線程的使用,多線程的難點就在線程之間的協調。在《操作系統》一課中,我們學習了進程,其實多線程和多進程一樣,都會涉及到多個進程或者線程對某一資源共享訪問的問題,當多個線程都需要修改這個資源的時候就會出現線程安全問題。
比如說在銀行開個賬戶會有一個存折和一張卡,如果某一天同一時間丈夫拿着存折去櫃台取錢,而妻子拿着銀行卡去ATM取錢。當丈夫查詢余額里面有3000元,正准備取2000元,這時候妻子也到ATM里面查詢也有3000,也取2000元。其實銀行不可能讓我們這么做,如果這樣的話那我們天天取錢去了,還搞什么工作啊。其實在丈夫查詢的時候已經對該賬號上了鎖,另外的銀行卡要取錢的話必須等待該鎖被釋放。下面用一個程序模擬這個例子:
1 package com.sync; 2 3 public class TestSync2 implements Runnable{ 4 public BankCard bc = new BankCard(); 5 public static void main(String[] args) { 6 TestSync2 test = new TestSync2(); 7 Thread wife = new Thread(test); 8 Thread husband = new Thread(test); 9 wife.setName("wife"); 10 husband.setName("husband"); 11 wife.start(); 12 husband.start(); 13 } 14 public void run() { 15 bc.getMoney(Thread.currentThread().getName(), 2000); 16 } 17 } 18 class BankCard{ 19 private static int money = 3000;//模擬賬戶余額 20 public synchronized void getMoney(String name,int m){ 21 //synchronized(this){ 22 try { 23 Thread.sleep(1); 24 } catch (InterruptedException e) { 25 e.printStackTrace(); 26 } 27 if(money > m){ 28 System.out.println(name+"取走了"+m+"元"); 29 money = money - m; 30 }else{ 31 System.out.println("對不起,您的余額不足!"); 32 } 33 //} 34 } 35 }
上面的例子如果在getMoney()方法上面不加synchronized關鍵字的話,輸出結果為:
wife取走了2000元
husband取走了2000元
而加上synchronized后,輸出結果為:
wife取走了2000元
對不起,您的余額不足!
上面兩種情況說明,如果多個線程同時訪問某個資源,而不給該資源枷鎖的話,就會出現問題。而加上synchronized關鍵字后就可以避免這種錯誤發生了。它能夠保證只有一個線程能夠訪問getMoney()這個方法,其他葯訪問該方法的線程必須等待。
鎖住某個資源可以用synchronized關鍵字來修飾一個方法或者同步代碼塊,這樣能保證同一時間只能由一個線程訪問該資源。
①、當兩個並發線程訪問同一個對象object中的這個synchronized(this)同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以后才能執行該代碼塊。
②、然而,當一個線程訪問object的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該object中的非synchronized(this)同步代碼塊。
③、尤其關鍵的是,當一個線程訪問object的一個synchronized(this)同步代碼塊時,其他線程對object中所有其它synchronized(this)同步代碼塊的訪問將被阻塞。
我們都知道,操作系統中多個進程之間如果不進行協調就很容易出現死鎖的情況,死鎖的四個條件:互斥、占有等待、非剝奪、循環等待。我們只要破壞其中一個條件就能避免死鎖發生。線程之間也容易出現死鎖,下面這個例子就演示了死鎖的情況:
1 package com.sync; 2 3 import com.thread.SleepTest; 4 5 6 public class TestDeadLock implements Runnable{ 7 int flag = 1; 8 static Object o1 = new Object(); 9 static Object o2 = new Object(); 10 public void run() { 11 System.out.println(flag); 12 if(flag == 1){ 13 synchronized (o1) { 14 try { 15 Thread.sleep(1000); 16 } catch (InterruptedException e) { 17 e.printStackTrace(); 18 } 19 synchronized (o2) { 20 System.out.println("1"); 21 } 22 } 23 } 24 if(flag == 0){ 25 synchronized (o2) {//鎖住某個對象,相當於占有該對象不讓其他人使用 26 try { 27 Thread.sleep(1000); 28 } catch (InterruptedException e) { 29 e.printStackTrace(); 30 } 31 synchronized (o1) { 32 System.out.println("0"); 33 } 34 } 35 } 36 } 37 public static void main(String[] args) { 38 TestDeadLock t1 = new TestDeadLock(); 39 TestDeadLock t2 = new TestDeadLock(); 40 t1.flag = 1; 41 t2.flag = 0; 42 new Thread(t1).start(); 43 new Thread(t2).start(); 44 } 45 }
運行程序輸出1 0后就進入死鎖狀態,該程序永遠也不會停止,因為兩個線程同時處於等待狀態。線程t1鎖住了o1對象,等待o2對象,而線程t2鎖住o2等待o2對象,誰也不讓誰,這就進入了一個循環占有等待的情況了,死鎖也就出現了。
所以,如果多個線程如果不進行協調的話很容易出現死鎖的問題。操作系統中使用進程調度來協調各個進程,那么java重如何對各個線程進行協調呢?
java中主要使用Object類中的wait()、notify()、notifyAll()方法來協調各個線程。典型的例子有哲學家吃飯問題、生產者和消費者問題、理發師問題。下面一個用一個例子來演示生產者和消費者問題。
問題描述:生產者負責做饅頭,做好饅頭后放進指定的簍子里面,消費者消費該簍子里面的饅頭。簍子里只能裝一定量的饅頭,滿了以后生產者必須進入等待狀態,消費者吃完饅頭后也必須進入等待狀態。
1 package com.sync; 2 3 public class ProductAndConsumer { 4 public static void main(String[] args) { 5 Basket b = new Basket(); 6 Product p = new Product(b); 7 Consumer c = new Consumer(b); 8 new Thread(p).start(); 9 new Thread(c).start(); 10 } 11 } 12 13 class ManTou{ 14 int id; 15 public ManTou(int id) { 16 this.id = id; 17 } 18 @Override 19 public String toString() { 20 return "ManTou"+id; 21 } 22 } 23 24 //裝饅頭的籃子 25 class Basket{ 26 int index = 0; //相當於棧頂指針 27 ManTou[] manTous = new ManTou[6]; 28 //往籃子里面放饅頭 29 public synchronized void push(ManTou m){ 30 while(index == manTous.length){ 31 try { 32 this.wait(); 33 } catch (InterruptedException e) { 34 e.printStackTrace(); 35 } 36 } 37 this.notify(); 38 manTous[index] = m; 39 index++; 40 } 41 //往籃子里面取饅頭 42 public synchronized ManTou pop(){ 43 while(index == 0){ 44 try { 45 this.wait(); 46 } catch (InterruptedException e) { 47 e.printStackTrace(); 48 } 49 } 50 this.notify(); 51 index--; 52 return manTous[index]; 53 } 54 } 55 //生產者 56 class Product implements Runnable{ 57 Basket basket; 58 public Product(Basket basket) { 59 this.basket = basket; 60 } 61 public void run() { 62 for (int i = 0; i < 20; i++) { 63 ManTou m = new ManTou(i); 64 basket.push(m); 65 System.out.println("生產了"+m); 66 try { 67 Thread.sleep(1); 68 } catch (InterruptedException e) { 69 e.printStackTrace(); 70 } 71 72 } 73 } 74 } 75 76 //消費者 77 class Consumer implements Runnable{ 78 Basket basket; 79 public Consumer(Basket basket) { 80 this.basket = basket; 81 } 82 public void run() { 83 for (int i = 0; i < 20; i++) { 84 ManTou m = basket.pop(); 85 System.out.println("消費了"+m); 86 try { 87 Thread.sleep((int)(Math.random()*1000)); 88 } catch (InterruptedException e) { 89 e.printStackTrace(); 90 } 91 } 92 } 93 }
wait()、notify()、notifyAll()方法的作用:
wait():導致當前的線程等待,直到其他線程調用此對象的 notify()
方法或 notifyAll()
方法。
notify():喚醒在此對象監視器上等待的單個線程。
notifyAll():喚醒在此對象監視器上等待的所有線程。
wait()與sleep()的區別:
兩個方法的共同點就是讓當前線程進入等待狀態。
不同點:
wait()之后,鎖就不歸我所有了,必須等醒過來后才能擁有該鎖,並且必須要有人喚醒它才會醒過來
sleep()不同,鎖還是歸我所有,一段時間后會自動醒過來