synchronized 與 Lock 的那點事

鴨脖發表於2016-07-28

最近在做一個監控系統,該系統主要包括對資料實時分析和儲存兩個部分,由於併發量比較高,所以不可避免的使用到了一些併發的知識。為了實現這些要求,後臺使用一個佇列作為快取,對於請求只管往快取裡寫資料。同時啟動一個執行緒監聽該佇列,檢測到資料,立即請求排程執行緒,對資料進行處理。 具體的使用方案就是使用同步保證資料的正常,使用執行緒池提高效率。

 
同步的實現當然是採用鎖了,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是個不錯的方案。
 
 
-------------------------------<全文完>-------------------------------------------------------------------------------------------------------

相關文章