【Java併發編程】之synchronized和Lock


Synchronized介紹


轉載請註明出處:http://blog.csdn.net/ns_code/article/details/17225469


   Java中,可以通過配合調用Object對象的wait()方法和notify()方法或notifyAll()方法來實現線程間的通信。在線程中調用wait()方法,將阻塞等待其他線程的通知(其他線程調用notify()方法或notifyAll()方法),在線程中調用notify()方法或notifyAll()方法,將通知其他線程從wait()方法處返回。


      Object是所有類的超類,它有5個方法組成了等待/通知機制的核心:notify()、notifyAll()、wait()、wait(long)和wait(long,int)。在Java中,所有的類都從Object繼承而來,因此,所有的類都擁有這些共有方法可供使用。而且,由於他們都被聲明爲final,因此在子類中不能覆寫任何一個方法。

 

     這裏詳細說明一下各個方法在使用中需要注意的幾點:

 

      1、wait()

      public final void wait()  throws InterruptedException,IllegalMonitorStateException

     該方法用來將當前線程置入休眠狀態,直到接到通知或被中斷爲止。在調用wait()之前,線程必須要獲得該對象的對象級別鎖,即只能在同步方法或同步塊中調用wait()方法。進入wait()方法後,當前線程釋放鎖。在從wait()返回前,線程與其他線程競爭重新獲得鎖。如果調用wait()時,沒有持有適當的鎖,則拋出IllegalMonitorStateException,它是RuntimeException的一個子類,因此,不需要try-catch結構。

 

     2、notify()

     public final native void notify() throws IllegalMonitorStateException

        該方法也要在同步方法或同步塊中調用,即在調用前,線程也必須要獲得該對象的對象級別鎖,的如果調用notify()時沒有持有適當的鎖,也會拋出IllegalMonitorStateException。

     該方法用來通知那些可能等待該對象的對象鎖的其他線程。如果有多個線程等待,則線程規劃器任意挑選出其中一個wait()狀態的線程來發出通知,並使它等待獲取該對象的對象鎖(notify後,當前線程不會馬上釋放該對象鎖,wait所在的線程並不能馬上獲取該對象鎖,要等到程序退出synchronized代碼塊後,當前線程纔會釋放鎖,wait所在的線程也纔可以獲取該對象鎖),但不驚動其他同樣在等待被該對象notify的線程們。當第一個獲得了該對象鎖的wait線程運行完畢以後,它會釋放掉該對象鎖,此時如果該對象沒有再次使用notify語句,則即便該對象已經空閒,其他wait狀態等待的線程由於沒有得到該對象的通知,會繼續阻塞在wait狀態,直到這個對象發出一個notify或notifyAll。這裏需要注意:它們等待的是被notify或notifyAll,而不是鎖。這與下面的notifyAll()方法執行後的情況不同。 

 

     3、notifyAll()

     public final native void notifyAll() throws IllegalMonitorStateException

      該方法與notify()方法的工作方式相同,重要的一點差異是:

      notifyAll使所有原來在該對象上wait的線程統統退出wait的狀態(即全部被喚醒,不再等待notify或notifyAll,但由於此時還沒有獲取到該對象鎖,因此還不能繼續往下執行),變成等待獲取該對象上的鎖,一旦該對象鎖被釋放(notifyAll線程退出調用了notifyAll的synchronized代碼塊的時候),他們就會去競爭。如果其中一個線程獲得了該對象鎖,它就會繼續往下執行,在它退出synchronized代碼塊,釋放鎖後,其他的已經被喚醒的線程將會繼續競爭獲取該鎖,一直進行下去,直到所有被喚醒的線程都執行完畢。

 

     4、wait(long)和wait(long,int)

     顯然,這兩個方法是設置等待超時時間的,後者在超值時間上加上ns,精度也難以達到,因此,該方法很少使用。對於前者,如果在等待線程接到通知或被中斷之前,已經超過了指定的毫秒數,則它通過競爭重新獲得鎖,並從wait(long)返回。另外,需要知道,如果設置了超時時間,當wait()返回時,我們不能確定它是因爲接到了通知還是因爲超時而返回的,因爲wait()方法不會返回任何相關的信息。但一般可以通過設置標誌位來判斷,在notify之前改變標誌位的值,在wait()方法後讀取該標誌位的值來判斷,當然爲了保證notify不被遺漏,我們還需要另外一個標誌位來循環判斷是否調用wait()方法。

       深入理解:

   如果線程調用了對象的wait()方法,那麼線程便會處於該對象的等待池中,等待池中的線程不會去競爭該對象的鎖。

   當有線程調用了對象的notifyAll()方法(喚醒所有wait線程)或notify()方法(只隨機喚醒一個wait線程),被喚醒的的線程便會進入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖。

   優先級高的線程競爭到對象鎖的概率大,假若某線程沒有競爭到該對象鎖,它還會留在鎖池中,唯有線程再次調用wait()方法,它纔會重新回到等待池中。而競爭到對象鎖的線程則繼續往下執行,直到執行完了synchronized代碼塊,它會釋放掉該對象鎖,這時鎖池中的線程會繼續競爭該對象鎖。


synchronized 和 Lock區別


同步的實現當然是採用鎖了,java中使用鎖的兩個基本工具是 synchronized 和 Lock。
 
一直很喜歡synchronized,因爲使用它很方便。比如,需要對一個方法進行同步,那麼只需在方法的簽名添加一個synchronized關鍵字。
 
// 未同步的方法
public void test() {}
// 同步的方法
pubilc synchronized void test() {}
 
synchronized 也可以用在一個代碼塊上,看
 
public void test() {
     synchronized(obj) {
          System.out.println("===");
     }
}
 
synchronized 用在方法和代碼塊上有什麼區別呢?
 
synchronized 用在方法簽名上(以test爲例),當某個線程調用此方法時,會獲取該實例的對象鎖,方法未結束之前,其他線程只能去等待。當這個方法執行完時,纔會釋放對象鎖。其他線程纔有機會去搶佔這把鎖,去執行方法test,但是發生這一切的基礎應當是所有線程使用的同一個對象實例,才能實現互斥的現象。否則synchronized關鍵字將失去意義。
 
但是如果該方法爲類方法,即其修飾符爲static,那麼synchronized 意味着某個調用此方法的線程當前會擁有該類的鎖,只要該線程持續在當前方法內運行,其他線程依然無法獲得方法的使用權!
 
synchronized 用在代碼塊的使用方式:synchronized(obj){//todo code here}
 
當線程運行到該代碼塊內,就會擁有obj對象的對象鎖,如果多個線程共享同一個Object對象,那麼此時就會形成互斥!特別的,當obj == this時,表示當前調用該方法的實例對象。即
 
public void test() {
     ...
     synchronized(this) {
          // todo your code
     }
     ...
}
 
此時,其效果等同於
public synchronized void test() {
     // todo your code
}
 
使用synchronized代碼塊,可以只對需要同步的代碼進行同步,這樣可以大大的提高效率。
 
小結:
使用synchronized 代碼塊相比方法有兩點優勢:
1、可以只對需要同步的使用
2、與wait()/notify()/nitifyAll()一起使用時,比較方便
 
----------------------------------------------------------------------------------------------------------------------------------------------------------
 
wait() 與notify()/notifyAll()
 
這三個方法都是Object的方法,並不是線程的方法!
wait():釋放佔有的對象鎖,線程進入等待池,釋放cpu,而其他正在等待的線程即可搶佔此鎖,獲得鎖的線程即可運行程序。而sleep()不同的是,線程調用此方法後,會休眠一段時間,休眠期間,會暫時釋放cpu,但並不釋放對象鎖。也就是說,在休眠期間,其他線程依然無法進入此代碼內部。休眠結束,線程重新獲得cpu,執行代碼。wait()和sleep()最大的不同在於wait()會釋放對象鎖,而sleep()不會!
 
notify(): 該方法會喚醒因爲調用對象的wait()而等待的線程,其實就是對對象鎖的喚醒,從而使得wait()的線程可以有機會獲取對象鎖。調用notify()後,並不會立即釋放鎖,而是繼續執行當前代碼,直到synchronized中的代碼全部執行完畢,纔會釋放對象鎖。JVM則會在等待的線程中調度一個線程去獲得對象鎖,執行代碼。需要注意的是,wait()和notify()必須在synchronized代碼塊中調用
 
notifyAll()則是喚醒所有等待的線程。
 
爲了說明這一點,舉例如下:
兩個線程依次打印"A""B",總共打印10次。
 
public class Consumer implements Runnable {
 
     @Override
     public synchronized void run() {
            // TODO Auto-generated method stub
            int count = 10;
            while(count > 0) {
                 synchronized (Test. obj) {
                     
                     System. out.print( "B");
                     count --;
                     Test. obj.notify(); // 主動釋放對象鎖
                     
                      try {
                           Test. obj.wait();
                           
                     } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                           e.printStackTrace();
                     }
                }
           }
     }
}
 
public class Produce implements Runnable {
 
     @Override
     public void run() {
            // TODO Auto-generated method stub
            int count = 10;
            while(count > 0) {
                 synchronized (Test. obj) {
                     
                      //System.out.print("count = " + count);
                     System. out.print( "A");
                     count --;
                     Test. obj.notify();
                     
                      try {
                           Test. obj.wait();
                     } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                           e.printStackTrace();
                     }
                }
           }
     }
}
 
測試類如下:
 
public class Test {
 
     public static final Object obj = new Object();
     
     public static void main(String[] args) {
           
            new Thread( new Produce()).start();
            new Thread( new Consumer()).start();
           
     }
}
 
這裏使用static obj作爲鎖的對象,當線程Produce啓動時(假如Produce首先獲得鎖,則Consumer會等待),打印“A”後,會先主動釋放鎖,然後阻塞自己。Consumer獲得對象鎖,打印“B”,然後釋放鎖,阻塞自己,那麼Produce又會獲得鎖,然後...一直循環下去,直到count = 0.這樣,使用Synchronized和wait()以及notify()就可以達到線程同步的目的。
 
----------------------------------------------------------------------------------------------------------------------------------------------------------
 
除了wait()和notify()協作完成線程同步之外,使用Lock也可以完成同樣的目的。
 
ReentrantLock 與synchronized有相同的併發性和內存語義,還包含了中斷鎖等候和定時鎖等候,意味着線程A如果先獲得了對象obj的鎖,那麼線程B可以在等待指定時間內依然無法獲取鎖,那麼就會自動放棄該鎖。
 
但是由於synchronized是在JVM層面實現的,因此係統可以監控鎖的釋放與否,而ReentrantLock使用代碼實現的,系統無法自動釋放鎖,需要在代碼中finally子句中顯式釋放鎖lock.unlock();
 
同樣的例子,使用lock 如何實現呢?
 
public class Consumer implements Runnable {
 
     private Lock lock;
     public Consumer(Lock lock) {
            this. lock = lock;
     }
     @Override
     public void run() {
            // TODO Auto-generated method stub
            int count = 10;
            while( count > 0 ) {
                 try {
                      lock.lock();
                     count --;
                     System. out.print( "B");
                } finally {
                      lock.unlock(); //主動釋放鎖
                      try {
                           Thread. sleep(91L);
                     } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                           e.printStackTrace();
                     }
                }
           }
 
     }
 
}
 
public class Producer implements Runnable{
 
     private Lock lock;
     public Producer(Lock lock) {
            this. lock = lock;
     }
     @Override
     public void run() {
            // TODO Auto-generated method stub
            int count = 10;
            while (count > 0) {
                 try {
                      lock.lock();
                     count --;
                     System. out.print( "A");
                } finally {
                      lock.unlock();
                      try {
                           Thread. sleep(90L);
                     } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                           e.printStackTrace();
                     }
                }
           }
     }
}
 
調用代碼:
 
public class Test {
 
     public static void main(String[] args) {
           Lock lock = new ReentrantLock();
           
           Consumer consumer = new Consumer(lock);
           Producer producer = new Producer(lock);
           
            new Thread(consumer).start();
            new Thread( producer).start();
           
     }
}
 
 
使用建議:
 
在併發量比較小的情況下,使用synchronized是個不錯的選擇,但是在併發量比較高的情況下,其性能下降很嚴重,此時ReentrantLock是個不錯的方案。
 
 
-------------------------------<全文完>-------------------------------------------------------------------------------------------------------


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章