【Java線程】鎖機制:synchronized、Lock、Condition轉載
http://www.infoq.com/cn/articles/java-memory-model-5 深入理解Java內存模型(五)——鎖
http://www.ibm.com/developerworks/cn/java/j-jtp10264/ Java 理論與實踐: JDK 5.0 中更靈活、更具可伸縮性的鎖定機制
http://blog.csdn.net/ghsau/article/details/7481142
1、synchronized
把代碼塊聲明為 synchronized,有兩個重要後果,通常是指該代碼具有 原子性(atomicity)和 可見性(visibility)。
1.1 原子性
原子性意味著個時刻,只有一個線程能夠執行一段代碼,這段代碼通過一個monitor object保護。從而防止多個線程在更新共享狀態時相互沖突。
1.2 可見性
可見性則更為微妙,它要對付內存緩存和編譯器優化的各種反常行為。它必須確保釋放鎖之前對共享數據做出的更改對於隨後獲得該鎖的另一個線程是可見的 。
作用:如果沒有同步機制提供的這種可見性保證,線程看到的共享變量可能是修改前的值或不一致的值,這將引發許多嚴重問題。
原理:當對象獲取鎖時,它首先使自己的高速緩存無效,這樣就可以保證直接從主內存中裝入變量。 同樣,在對象釋放鎖之前,它會刷新其高速緩存,強制使已做的任何更改都出現在主內存中。 這樣,會保證在同一個鎖上同步的兩個線程看到在 synchronized 塊內修改的變量的相同值。
一般來說,線程以某種不必讓其他線程立即可以看到的方式(不管這些線程在寄存器中、在處理器特定的緩存中,還是通過指令重排或者其他編譯器優化),不受緩存變量值的約束,但是如果開發人員使用了同步,那麽運行庫將確保某一線程對變量所做的更新先於對現有synchronized 塊所進行的更新,當進入由同一監控器(lock)保護的另一個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 可以花更少的時候來調度線程,把更多時間用在執行線程上。)
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,則代碼如下:
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);
}
}
然後寫個測試類來用多個線程分別讀寫這個共享數據:
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();
}
}
運行結果:
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;
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();// 釋放讀鎖
}
}
}
測試結果:
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的功能
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不知道喚醒的是讀線程還是寫線程了,如果喚醒的是讀線程,皆大歡喜,如果喚醒的是寫線程,那麽線程剛被喚醒,又被阻塞了,這時又去喚醒,這樣就浪費了很多時間。
【Java線程】鎖機制:synchronized、Lock、Condition轉載