Java 線程鎖機制 -Synchronized Lock 互斥鎖 讀寫鎖

(1)synchronized 是互斥鎖;html

(2)ReentrantLock 顧名思義 :可重入鎖java

(3)ReadWriteLock :讀寫鎖算法

讀寫鎖特色:緩存

a)多個讀者能夠同時進行讀
b)寫者必須互斥(只容許一個寫者寫,也不能讀者寫者同時進行)
c)寫者優先於讀者(一旦有寫者,則後續讀者必須等待,喚醒時優先考慮寫者)數據結構

 

一、synchronized

把代碼塊聲明爲 synchronized,有兩個重要後果,一般是指該代碼具備 原子性(atomicity)和 可見性(visibility)多線程

(1) 原子性併發

原子性意味着個時刻,只有一個線程可以執行一段代碼,這段代碼經過一個monitor object保護。從而防止多個線程在更新共享狀態時相互衝突。框架

 (2)  可見性dom

可見性則更爲微妙,它要對付內存緩存和編譯器優化的各類反常行爲。它必須確保釋放鎖以前對共享數據作出的更改對於隨後得到該鎖的另外一個線程是可見的 。ide

做用:若是沒有同步機制提供的這種可見性保證,線程看到的共享變量多是修改前的值或不一致的值,這將引起許多嚴重問題。 

通常來講,線程以某種沒必要讓其餘線程當即能夠看到的方式(無論這些線程在寄存器中、在處理器特定的緩存中,仍是經過指令重排或者其餘編譯器優化),不受緩存變量值的約束,可是若是開發人員使用了同步,那麼運行庫將確保某一線程對變量所作的更新先於對現有synchronized 塊所進行的更新,當進入由同一監控器(lock)保護的另外一個synchronized 塊時,將馬上能夠看到這些對變量所作的更新。相似的規則也存在於volatile變量上。

——volatile只保證可見性,不保證原子性! 

(3)synchronize的限制

synchronized是不錯,但它並不完美。它有一些功能性的限制:

  1. 它沒法中斷一個正在等候得到鎖的線程;
  2. 也沒法經過投票獲得鎖,若是不想等下去,也就無法獲得鎖; 

二、ReentrantLock (可重入鎖)

Java.util.concurrent.lock 中的Lock 框架是鎖定的一個抽象,它容許把鎖定的實現做爲 Java 類,而不是做爲語言的特性來實現。這就爲Lock 的多種實現留下了空間,各類實現可能有不一樣的調度算法、性能特性或者鎖定語義。

ReentrantLock 類實現了Lock ,它擁有與synchronized 相同的併發性和內存語義,可是添加了相似鎖投票定時鎖等候可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用狀況下更佳的性能。(換句話說,當許多線程都想訪問共享資源時,JVM 能夠花更少的時候來調度線程,把更多時間用在執行線程上。) 

 1 class Outputter1 {    
 2     private Lock lock = new ReentrantLock();// 鎖對象    
 3   
 4     public void output(String name) {           
 5         lock.lock();      // 獲得鎖    
 6   
 7         try {    
 8             for(int i = 0; i < name.length(); i++) {    
 9                 System.out.print(name.charAt(i));    
10             }    
11         } finally {    
12             lock.unlock();// 釋放鎖    
13         }    
14     }    
15 }

區別:

   須要注意的是,用sychronized修飾的方法或者語句塊在代碼執行完以後鎖自動釋放,而是用Lock須要咱們手動釋放鎖,因此爲了保證鎖最終被釋放(發生異常狀況),要把互斥區放在try內,釋放鎖放在finally內!! 

三、讀寫鎖ReadWriteLock

上例中展現的是和synchronized相同的功能,那Lock的優點在哪裏? 

例如一個類對其內部共享數據data提供了get()和set()方法,若是用synchronized,則代碼以下:

 1 class syncData {        
 2     private int data;// 共享數據        
 3     public synchronized void set(int data) {    
 4         System.out.println(Thread.currentThread().getName() + "準備寫入數據");    
 5         try {    
 6             Thread.sleep(20);    
 7         } catch (InterruptedException e) {    
 8             e.printStackTrace();    
 9         }    
10         this.data = data;    
11         System.out.println(Thread.currentThread().getName() + "寫入" + this.data);    
12     }       
13     public synchronized  void get() {    
14         System.out.println(Thread.currentThread().getName() + "準備讀取數據");    
15         try {    
16             Thread.sleep(20);    
17         } catch (InterruptedException e) {    
18             e.printStackTrace();    
19         }    
20         System.out.println(Thread.currentThread().getName() + "讀取" + this.data);    
21     }    
22 }

而後寫個測試類來用多個線程分別讀寫這個共享數據:

public static void main(String[] args) {    
//        final Data data = new Data();    
          final syncData data = new syncData();    
//        final RwLockData data = new RwLockData();    
          
        //寫入  
        for (int i = 0; i < 3; i++) {    
            Thread t = new Thread(new Runnable() {    
                @Override  
        public void run() {    
                    for (int j = 0; j < 5; j++) {    
                        data.set(new Random().nextInt(30));    
                    }    
                }    
            });  
            t.setName("Thread-W" + i);  
            t.start();  
        }    
        //讀取  
        for (int i = 0; i < 3; i++) {    
            Thread t = new Thread(new Runnable() {    
                @Override  
        public void run() {    
                    for (int j = 0; j < 5; j++) {    
                        data.get();    
                    }    
                }    
            });    
            t.setName("Thread-R" + i);  
            t.start();  
        }    
    }

運行結果: 

 1 Thread-R2準備讀取數據  
 2 Thread-R2讀取1  
 3 Thread-R2準備讀取數據  
 4 Thread-R2讀取1  
 5 Thread-R2準備讀取數據  
 6 Thread-R2讀取1  
 7 Thread-R2準備讀取數據  
 8 Thread-R2讀取1  
 9 Thread-R0準備讀取數據 //R0和R2能夠同時讀取,不該該互斥!  
10 Thread-R0讀取1  
11 Thread-R0準備讀取數據  
12 Thread-R0讀取1  
13 Thread-R0準備讀取數據  
14 Thread-R0讀取1  
15 Thread-R0準備讀取數據

如今一切都看起來很好!各個線程互不干擾!等等。。讀取線程和寫入線程互不干擾是正常的,可是兩個讀取線程是否須要互不干擾??

對!讀取線程不該該互斥!

咱們能夠用讀寫鎖ReadWriteLock實現:

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

 1 class Data {        
 2     private int data;// 共享數據    
 3     private ReadWriteLock rwl = new ReentrantReadWriteLock();       
 4     public void set(int data) {    
 5         rwl.writeLock().lock();// 取到寫鎖    
 6         try {    
 7             System.out.println(Thread.currentThread().getName() + "準備寫入數據");    
 8             try {    
 9                 Thread.sleep(20);    
10             } catch (InterruptedException e) {    
11                 e.printStackTrace();    
12             }    
13             this.data = data;    
14             System.out.println(Thread.currentThread().getName() + "寫入" + this.data);    
15         } finally {    
16             rwl.writeLock().unlock();// 釋放寫鎖    
17         }    
18     }       
19   
20     public void get() {    
21         rwl.readLock().lock();// 取到讀鎖    
22         try {    
23             System.out.println(Thread.currentThread().getName() + "準備讀取數據");    
24             try {    
25                 Thread.sleep(20);    
26             } catch (InterruptedException e) {    
27                 e.printStackTrace();    
28             }    
29             System.out.println(Thread.currentThread().getName() + "讀取" + this.data);    
30         } finally {    
31             rwl.readLock().unlock();// 釋放讀鎖    
32         }    
33     }    
34 }

測試結果:

 1 Thread-W1準備寫入數據  
 2 Thread-W1寫入9  
 3 Thread-W1準備寫入數據  
 4 Thread-W1寫入24  
 5 Thread-W1準備寫入數據  
 6 Thread-W1寫入12  
 7 Thread-W0準備寫入數據  
 8 Thread-W0寫入22  
 9 Thread-W0準備寫入數據  
10 Thread-W0寫入15  
11 Thread-W0準備寫入數據  
12 Thread-W0寫入6  
13 Thread-W0準備寫入數據  
14 Thread-W0寫入13  
15 Thread-W0準備寫入數據  
16 Thread-W0寫入0  
17 Thread-W2準備寫入數據  
18 Thread-W2寫入23  
19 Thread-W2準備寫入數據  
20 Thread-W2寫入24  
21 Thread-W2準備寫入數據  
22 Thread-W2寫入24  
23 Thread-W2準備寫入數據  
24 Thread-W2寫入17  
25 Thread-W2準備寫入數據  
26 Thread-W2寫入11  
27 Thread-R2準備讀取數據  
28 Thread-R1準備讀取數據  
29 Thread-R0準備讀取數據  
30 Thread-R0讀取11  
31 Thread-R1讀取11  
32 Thread-R2讀取11  
33 Thread-W1準備寫入數據  
34 Thread-W1寫入18  
35 Thread-W1準備寫入數據  
36 Thread-W1寫入1  
37 Thread-R0準備讀取數據  
38 Thread-R2準備讀取數據  
39 Thread-R1準備讀取數據  
40 Thread-R2讀取1

與互斥鎖定相比,讀-寫鎖定容許對共享數據進行更高級別的併發訪問。雖然一次只有一個線程(writer 線程)能夠修改共享數據,但在許多狀況下,任何數量的線程能夠同時讀取共享數據(reader 線程) 

從理論上講,與互斥鎖定相比,使用讀-寫鎖定所容許的併發性加強將帶來更大的性能提升。

在實踐中,只有在多處理器上而且只在訪問模式適用於共享數據時,才能徹底實現併發性加強。——例如,某個最初用數據填充而且以後不常常對其進行修改的 collection,由於常常對其進行搜索(好比搜索某種目錄),因此這樣的 collection 是使用讀-寫鎖定的理想候選者。

 

四、線程間通訊Condition

Condition能夠替代傳統的線程間通訊,用await()替換wait(),用signal()替換notify(),用signalAll()替換notifyAll()。

——爲何方法名不直接叫wait()/notify()/nofityAll()?由於Object的這幾個方法是final的,不可重寫!

 

傳統線程的通訊方式,Condition均可以實現。

注意,Condition是被綁定到Lock上的,要建立一個Lock的Condition必須用newCondition()方法。

 

Condition的強大之處在於它能夠爲多個線程間創建不一樣的Condition

看JDK文檔中的一個例子:假定有一個綁定的緩衝區,它支持 put 和 take 方法。若是試圖在空的緩衝區上執行take 操做,則在某一個項變得可用以前,線程將一直阻塞;若是試圖在滿的緩衝區上執行 put 操做,則在有空間變得可用以前,線程將一直阻塞。咱們喜歡在單獨的等待 set 中保存put 線程和take 線程,這樣就能夠在緩衝區中的項或空間變得可用時利用最佳規劃,一次只通知一個線程。可使用兩個Condition 實例來作到這一點。

——其實就是java.util.concurrent.ArrayBlockingQueue的功能

優勢:假設緩存隊列中已經存滿,那麼阻塞的確定是寫線程,喚醒的確定是讀線程,相反,阻塞的確定是讀線程,喚醒的確定是寫線程。

相關文章
相關標籤/搜索