【Java執行緒鎖機制】:synchronized、Lock、Condition
原文:http://blog.csdn.net/vking_wang/article/details/9952063
1、synchronized
把程式碼塊宣告為 synchronized,有兩個重要後果,通常是指該程式碼具有 原子性(atomicity)和 可見性(visibility)。
1.1 原子性
原子性意味著個時刻,只有一個執行緒能夠執行一段程式碼,這段程式碼通過一個monitor object保護。從而防止多個執行緒在更新共享狀態時相互衝突。
1.2 可見性
可見性則更為微妙,它要對付記憶體快取和編譯器優化的各種反常行為。它必須確保釋放鎖之前對共享資料做出的更改對於隨後獲得該鎖的另一個執行緒是可見的 。
作用:如果沒有同步機制提供的這種可見性保證,執行緒看到的共享變數可能是修改前的值或不一致的值,這將引發許多嚴重問題。
原理:當物件獲取鎖時,它首先使自己的快取記憶體無效,這樣就可以保證直接從主記憶體中裝入變數。 同樣,在物件釋放鎖之前,它會重新整理其快取記憶體,強制使已做的任何更改都出現在主記憶體中。 這樣,會保證在同一個鎖上同步的兩個執行緒看到在 synchronized 塊內修改的變數的相同值。
一般來說,執行緒以某種不必讓其他執行緒立即可以看到的方式(不管這些執行緒在暫存器中、在處理器特定的快取中,還是通過指令重排或者其他編譯器優化),不受快取變數值的約束,但是如果開發人員使用了同步,那麼執行庫將確保某一執行緒對變數所做的更新先於對現有synchronized
synchronized
塊時,將立刻可以看到這些對變數所做的更新。類似的規則也存在於volatile
變數上。
——volatile只保證可見性,不保證原子性!
1.3 何時要同步?
可見性同步的基本規則是在以下情況中必須同步:
- 讀取上一次可能是由另一個執行緒寫入的變數
- 寫入下一次可能由另一個執行緒讀取的變數
一致性同步:當修改多個相關值時,您想要其它執行緒原子地看到這組更改—— 要麼看到全部更改,要麼什麼也看不到。
這適用於相關資料項(如粒子的位置和速率)和元資料項(如連結串列中包含的資料值和列表自身中的資料項的鏈)。
在某些情況中,您不必用同步來將資料從一個執行緒傳遞到另一個,因為 JVM 已經隱含地為您執行同步。這些情況包括:
- 由靜態初始化器(在靜態欄位上或 static{} 塊中的初始化器)
- 初始化資料時
- 訪問 final 欄位時 ——final物件呢?
- 在建立執行緒之前建立物件時
- 執行緒可以看見它將要處理的物件時
1.4 synchronize的限制
synchronized是不錯,但它並不完美。它有一些功能性的限制:
- 它無法中斷一個正在等候獲得鎖的執行緒;
- 也無法通過投票得到鎖,如果不想等下去,也就沒法得到鎖;
- 同步還要求鎖的釋放只能在與獲得鎖所在的堆疊幀相同的堆疊幀中進行,多數情況下,這沒問題(而且與異常處理互動得很好),但是,確實存在一些非塊結構的鎖定更合適的情況。
2、ReentrantLock
java.util.concurrent.lock
中的Lock
框架是鎖定的一個抽象,它允許把鎖定的實現作為 Java 類,而不是作為語言的特性來實現。這就為Lock
的多種實現留下了空間,各種實現可能有不同的排程演算法、效能特性或者鎖定語義。
ReentrantLock
類實現了Lock
,它擁有與synchronized
相同的併發性和記憶體語義,但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的效能。(換句話說,當許多執行緒都想訪問共享資源時,JVM 可以花更少的時候來排程執行緒,把更多時間用在執行執行緒上。)
[java] view plain copy print?
- class Outputter1 {
- private Lock lock = new ReentrantLock();// 鎖物件
- public void output(String name) {
- lock.lock(); // 得到鎖
- try {
- for(int i = 0; i < name.length(); i++) {
- System.out.print(name.charAt(i));
- }
- } finally {
- lock.unlock();// 釋放鎖
- }
- }
- }
區別:
需要注意的是,用sychronized修飾的方法或者語句塊在程式碼執行完之後鎖自動釋放,而是用Lock需要我們手動釋放鎖,所以為了保證鎖最終被釋放(發生異常情況),要把互斥區放在try內,釋放鎖放在finally內!!
3、讀寫鎖ReadWriteLock
上例中展示的是和synchronized相同的功能,那Lock的優勢在哪裡?
例如一個類對其內部共享資料data提供了get()和set()方法,如果用synchronized,則程式碼如下:
[java] view plain copy print?
- class syncData {
- private int data;// 共享資料
- public synchronized void set(int data) {
- System.out.println(Thread.currentThread().getName() + "準備寫入資料");
- try {
- Thread.sleep(20);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- this.data = data;
- System.out.println(Thread.currentThread().getName() + "寫入" + this.data);
- }
- public synchronized void get() {
- System.out.println(Thread.currentThread().getName() + "準備讀取資料");
- try {
- Thread.sleep(20);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName() + "讀取" + this.data);
- }
- }
然後寫個測試類來用多個執行緒分別讀寫這個共享資料:
[java] view plain copy print?
- 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();
- }
- }
執行結果:
[plain] view plain copy print?
- Thread-W0準備寫入資料
- Thread-W0寫入0
- Thread-W0準備寫入資料
- Thread-W0寫入1
- Thread-R1準備讀取資料
- Thread-R1讀取1
- Thread-R1準備讀取資料
- Thread-R1讀取1
- Thread-R1準備讀取資料
- Thread-R1讀取1
- Thread-R1準備讀取資料
- Thread-R1讀取1
- Thread-R1準備讀取資料
- Thread-R1讀取1
- Thread-R2準備讀取資料
- Thread-R2讀取1
- Thread-R2準備讀取資料
- Thread-R2讀取1
- Thread-R2準備讀取資料
- Thread-R2讀取1
- Thread-R2準備讀取資料
- Thread-R2讀取1
- Thread-R2準備讀取資料
- Thread-R2讀取1
- Thread-R0準備讀取資料 //R0和R2可以同時讀取,不應該互斥!
- Thread-R0讀取1
- Thread-R0準備讀取資料
- Thread-R0讀取1
- Thread-R0準備讀取資料
- Thread-R0讀取1
- Thread-R0準備讀取資料
- Thread-R0讀取1
- Thread-R0準備讀取資料
- Thread-R0讀取1
- Thread-W1準備寫入資料
- Thread-W1寫入18
- Thread-W1準備寫入資料
- Thread-W1寫入16
- Thread-W1準備寫入資料
- Thread-W1寫入19
- Thread-W1準備寫入資料
- Thread-W1寫入21
- Thread-W1準備寫入資料
- Thread-W1寫入4
- Thread-W2準備寫入資料
- Thread-W2寫入10
- Thread-W2準備寫入資料
- Thread-W2寫入4
- Thread-W2準備寫入資料
- Thread-W2寫入1
- Thread-W2準備寫入資料
- Thread-W2寫入14
- Thread-W2準備寫入資料
- Thread-W2寫入2
- Thread-W0準備寫入資料
- Thread-W0寫入4
- Thread-W0準備寫入資料
- Thread-W0寫入20
- Thread-W0準備寫入資料
- Thread-W0寫入29
現在一切都看起來很好!各個執行緒互不干擾!等等。。讀取執行緒和寫入執行緒互不干擾是正常的,但是兩個讀取執行緒是否需要互不干擾??
對!讀取執行緒不應該互斥!
我們可以用讀寫鎖ReadWriteLock實現:
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
[java] view plain copy print?
- class Data {
- private int data;// 共享資料
- private ReadWriteLock rwl = new ReentrantReadWriteLock();
- public void set(int data) {
- rwl.writeLock().lock();// 取到寫鎖
- try {
- System.out.println(Thread.currentThread().getName() + "準備寫入資料");
- try {
- Thread.sleep(20);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- this.data = data;
- System.out.println(Thread.currentThread().getName() + "寫入" + this.data);
- } finally {
- rwl.writeLock().unlock();// 釋放寫鎖
- }
- }
- public void get() {
- rwl.readLock().lock();// 取到讀鎖
- try {
- System.out.println(Thread.currentThread().getName() + "準備讀取資料");
- try {
- Thread.sleep(20);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- System.out.println(Thread.currentThread().getName() + "讀取" + this.data);
- } finally {
- rwl.readLock().unlock();// 釋放讀鎖
- }
- }
- }
測試結果:
[plain] view plain copy print?
- Thread-W1準備寫入資料
- Thread-W1寫入9
- Thread-W1準備寫入資料
- Thread-W1寫入24
- Thread-W1準備寫入資料
- Thread-W1寫入12
- Thread-W0準備寫入資料
- Thread-W0寫入22
- Thread-W0準備寫入資料
- Thread-W0寫入15
- Thread-W0準備寫入資料
- Thread-W0寫入6
- Thread-W0準備寫入資料
- Thread-W0寫入13
- Thread-W0準備寫入資料
- Thread-W0寫入0
- Thread-W2準備寫入資料
- Thread-W2寫入23
- Thread-W2準備寫入資料
- Thread-W2寫入24
- Thread-W2準備寫入資料
- Thread-W2寫入24
- Thread-W2準備寫入資料
- Thread-W2寫入17
- Thread-W2準備寫入資料
- Thread-W2寫入11
- Thread-R2準備讀取資料
- Thread-R1準備讀取資料
- Thread-R0準備讀取資料
- Thread-R0讀取11
- Thread-R1讀取11
- Thread-R2讀取11
- Thread-W1準備寫入資料
- Thread-W1寫入18
- Thread-W1準備寫入資料
- Thread-W1寫入1
- Thread-R0準備讀取資料
- Thread-R2準備讀取資料
- Thread-R1準備讀取資料
- Thread-R2讀取1
- Thread-R2準備讀取資料
- Thread-R1讀取1
- Thread-R0讀取1
- Thread-R1準備讀取資料
- Thread-R0準備讀取資料
- Thread-R0讀取1
- Thread-R2讀取1
- Thread-R2準備讀取資料
- Thread-R1讀取1
- Thread-R0準備讀取資料
- Thread-R1準備讀取資料
- Thread-R0讀取1
- Thread-R2讀取1
- Thread-R1讀取1
- Thread-R0準備讀取資料
- Thread-R1準備讀取資料
- Thread-R2準備讀取資料
- Thread-R1讀取1
- Thread-R2讀取1
- Thread-R0讀取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的功能
[java] view plain copy print?
- class BoundedBuffer {
- final Lock lock = new ReentrantLock(); //鎖物件
- final Condition notFull = lock.newCondition(); //寫執行緒鎖
- final Condition notEmpty = lock.newCondition(); //讀執行緒鎖
- final Object[] items = new Object[100];//快取佇列
- int putptr; //寫索引
- int takeptr; //讀索引
- int count; //佇列中資料數目
- //寫
- public void put(Object x) throws InterruptedException {
- lock.lock(); //鎖定
- try {
- // 如果佇列滿,則阻塞<寫執行緒>
- while (count == items.length) {
- notFull.await();
- }
- // 寫入佇列,並更新寫索引
- items[putptr] = x;
- if (++putptr == items.length) putptr = 0;
- ++count;
- // 喚醒<讀執行緒>
- notEmpty.signal();
- } finally {
- lock.unlock();//解除鎖定
- }
- }
- //讀
- public Object take() throws InterruptedException {
- lock.lock(); //鎖定
- try {
- // 如果佇列空,則阻塞<讀執行緒>
- while (count == 0) {
- notEmpty.await();
- }
- //讀取佇列,並更新讀索引
- Object x = items[takeptr];
- if (++takeptr == items.length) takeptr = 0;
- --count;
- // 喚醒<寫執行緒>
- notFull.signal();
- return x;
- } finally {
- lock.unlock();//解除鎖定
- }
- }
優點:
假設快取佇列中已經存滿,那麼阻塞的肯定是寫執行緒,喚醒的肯定是讀執行緒,相反,阻塞的肯定是讀執行緒,喚醒的肯定是寫執行緒。
那麼假設只有一個Condition會有什麼效果呢?快取佇列中已經存滿,這個Lock不知道喚醒的是讀執行緒還是寫執行緒了,如果喚醒的是讀執行緒,皆大歡喜,如果喚醒的是寫執行緒,那麼執行緒剛被喚醒,又被阻塞了,這時又去喚醒,這樣就浪費了很多時間。