(1)synchronized 是互斥鎖;
(2)ReentrantLock 顧名思義 :可重入鎖
(3)ReadWriteLock :讀寫鎖
讀寫鎖特點:
a)多個讀者可以同時進行讀
b)寫者必須互斥(只允許一個寫者寫,也不能讀者寫者同時進行)
c)寫者優先於讀者(一旦有寫者,則后續讀者必須等待,喚醒時優先考慮寫者)
1、synchronized
把代碼塊聲明為 synchronized,有兩個重要后果,通常是指該代碼具有 原子性(atomicity)和 可見性(visibility)。
(1) 原子性
原子性意味着個時刻,只有一個線程能夠執行一段代碼,這段代碼通過一個monitor object保護。從而防止多個線程在更新共享狀態時相互沖突。
(2) 可見性
可見性則更為微妙,它要對付內存緩存和編譯器優化的各種反常行為。它必須確保釋放鎖之前對共享數據做出的更改對於隨后獲得該鎖的另一個線程是可見的 。
作用:如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一致的值,這將引發許多嚴重問題。
一般來說,線程以某種不必讓其他線程立即可以看到的方式(不管這些線程在寄存器中、在處理器特定的緩存中,還是通過指令重排或者其他編譯器優化),不受緩存變量值的約束,但是如果開發人員使用了同步,那么運行庫將確保某一線程對變量所做的更新先於對現有synchronized
塊所進行的更新,當進入由同一監控器(lock)保護的另一個synchronized
塊時,將立刻可以看到這些對變量所做的更新。類似的規則也存在於volatile
變量上。
(3)synchronize的限制
synchronized是不錯,但它並不完美。它有一些功能性的限制:
- 它無法中斷一個正在等候獲得鎖的線程;
- 也無法通過投票得到鎖,如果不想等下去,也就沒法得到鎖;
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內!!
3、讀寫鎖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 是使用讀-寫鎖定的理想候選者。
4、線程間通信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的功能
優點:
假設緩存隊列中已經存滿,那么阻塞的肯定是寫線程,喚醒的肯定是讀線程,相反,阻塞的肯定是讀線程,喚醒的肯定是寫線程。