1. 面對instance 函數,synchronized 鎖定的是對象(objects)而非函數(methods)或代碼(code)。
Synchronized既可以作用於方法修飾,也可以用於方法內的修飾。對於instance 函數,關鍵詞synchronized 其實並不鎖定方法或代碼,它鎖定的是對象(至於synchronized 對statics 的影響,請見下面)。記住,每個對象只有一個lock(機鎖)與之相關聯。當synchronized 被當作函數修飾符的時候,它所取得的lock 將被交給函數調用者(某對象)。如果synchronized 用於object reference,則取得的lock 將被交給該reference所指對象。分析如下:
public class Test {
public synchronized void method1() {//修飾方法
}
public void method2() {//修飾object reference
synchronized (this) {
}
}
public void method3(Object object) {//修飾object reference
synchronized (object) {
}
}
}
前兩個函數method1()和method2()在[對象鎖定]方面功能一致。 二者都對this進行同步控制。換句話說,獲得的lock 將給予調用此函數的對象(也就是this)。由於這兩個函數都隸屬class Test,所以lock 由Test 的某個對象獲得。method3()則同步控制object所指的那個對象。
對一個對象進行同步控制到底意味什麼呢?它意味[調用該函數]之線程將會取得對象的lock。持有[對象A 之lock],意味另一個通過synchronized 函數或synchronized語句來申請[對象A 之lock]的線程,在該lock 被釋放之前將無法獲得滿足。然而如果另一個線程對對象A 所屬類之另一對象B 調用相同的synchronized 函數或synchronized 區塊,可以獲得[對象B 之lock]。因此,synchronized 函數或synchronized 區段內的代碼在同一時刻下可由多個線程執行,只要是對不同的對象調用該函數。
記住,同步機制(synchronization)鎖定的是對象,而不是函數或代碼。函數或代碼區段被聲明爲synchronization 並非意味它在同一時刻只能由一個線程執行。
最後一點說明是:Java 語言不允許你將構造函數聲明爲,synchronized(那麼做會發生編譯錯誤)。原因是當兩個線程併發調用同一個構造函數時,它們各自操控的是同一個class 的兩個不同實體(對象)的內存(也就是說synchronized 是畫蛇添足之舉)。然而如果在這些構造函數內包含了彼此競爭共享資源的代碼(比如說靜態變量),則必須同步控制那些資源以迴避衝突。
2. 弄滇楚synchronized statics函數(同步靜態函數)與synchronized instance函數(同步實例函數)之間的差異。
當調用synchronized statics方法時,獲取到的lock是與定義該方法的Class對象相關,而不是與調用該方法的對象有關。當你對一個class literal(類名稱字面常量)調用其synchronized 區段時,獲得的也是同樣那個lock,也就是[與特定Class 對象相關聯]的lock。
如下:
public class Thread01 {
public static synchronized void method() {
}
public void method1() {
synchronized (Thread01.class) {
}
}
}
method()和method1()都是爭取的同一個lock,也就是Thread01 Class object lock。method()通過synchronized的修飾符來獲取lock,而method2()是通過class literal Thread01.class來獲取lock的。
如果synchronized 施行於instance 函數和object references,得到的lock 就與前面的不一樣了。對於instance 函數,取得的lock 隸屬於其調用者(某個對象),至於同步控制一個(指名)對象,取得的當然是該對象的lock。
由於同步控制(1)instance 函數(2)static 函數(3)對象(object) (4)class literals 時得到的locks 不同,因此在決定互(mutual exclusion)行爲時一定要小心謹慎。記住,同步控制[通過instance 函數或object reference 所取得的lock]。完全不同於同步控制[通過static 函數或class literal 所取得的lock]。兩個函數被聲明爲synchronized 並不就意味它們具備多線程安全性。你必須小心識別和區分通過同步控制所取得的locks 之間的微妙差異。
如下:
class Thread02 implements Runnable {
public synchronized void printlnM1() {
while (true) {
System.out.println("printlnM1");
}
}
public static synchronized void printlnM2() {
while (true) {
System.out.println("printlnM2");
}
}
@Override
public void run() {
printlnM1();
}
}
class TestThread {
public static void main(String[] args) {
Thread02 t = new Thread02();
Thread f = new Thread(t);
f.start();
t.printlnM2();
}
}
儘管上述兩個函數都聲明爲synchronized,它們並非[多線程安全](thread safe)。
其原因在於一個是synchronized static 函數,另一個是synchronized instance函數。因此它們爭取的是不同的locks。instance 函數printlnM1()取得的是Thread02 object lock,static 函數printlnM2()取得的是Thread02 的Class object lock。這是不同的兩個locks,彼此互不影響。當上述代碼執行起來,兩個字符串都打印在屏幕上。換句話講,兩個函數的執行交互穿插。如果需要同步控制這段代碼,可以共享同一個資源。爲了保護這筆資源,代碼必須有正確的同步控制,以避免衝突。兩種選擇可以解決這個問題:
1.同步控制(synchronize)公用資源。
2. 同步控制(synchronize)—個特殊的instance 變量。
方案一實現:前提假設兩個函數要更新同一個對象,它們就對其進行同步控制。
class Thread02 implements Runnable {
private Object o;
public synchronized void printlnM1() {
synchronized (o) {
while (true) {
System.out.println("printlnM1");
}
}
}
public static synchronized void printlnM2(Thread02 o) {
synchronized (o.o) {
while (true) {
System.out.println("printlnM2");
}
}
}
@Override
public void run() {
printlnM1();
}
}
方案二實現:聲明一個local instance 變量,惟一的目的就是對它進行同步控制。
class Thread02 implements Runnable {
private byte[] lock = new byte[0];
public synchronized void printlnM1() {
synchronized (lock) {
while (true) {
System.out.println("printlnM1");
}
}
}
public static synchronized void printlnM2(Thread02 o) {
synchronized (o.lock) {
while (true) {
System.out.println("printlnM2");
}
}
}
@Override
public void run() {
printlnM1();
}
}由於只能鎖定對象,你使用的local instance 變量必須是個對象。
3. 以[private數據+相應的訪問函數(accessor)]替換[public/protected數據。這樣做是爲了封裝。記住,對於[在synchronized 函數中可被修改的數據],應使之成爲private,並根據需要提供訪問函數(accessor)。如果訪問函數返回是可變對象(mutable object),那麼應該先cloned(克隆)該對象。
4. 要避免無所謂的同步控制。過度的同步控制估計會的導致死鎖(deadlocks)或者併發(concurrency)度降低。同步機制對每個對象只提供一個lock。當一個函數聲明爲synchronized,所獲得的lock 乃是隸屬於調用此函數的那個對象。如果該對象要訪問其他方法的話,就必須釋放lock,這樣的話性能就降低了,這時可以再添加一個變量來產生不同的lock。如下:
class Thread03{
private int[] a1;
private int[] a2;
private double[] d1;
private double[] d2;
private byte[] a = new byte[0];
private byte[] d = new byte[0];
public void ma1(){
synchronized (a) {
}
}
public void ma2(){
synchronized (a) {
}
}
public void md1(){
synchronized (d) {
}
}
public void md2(){
synchronized (d) {
}
}
}
5. 訪問共享變量時請使用synchronized或volatile。可以確保變量與主內存完全保持一致,從而在任何時候得到正確數值。記住,一旦變量被聲明爲volatile,在每次訪問它們時,它們就與主內存進行一致化。但如果使用synchronized,只有在取得lock 和釋放lock 的時候,纔會對變量和主內存進行一致化。
|
優點 |
缺點 |
synchronized |
取得和釋放lock 時,進行私 有專用副本 與主內存正本的一致化 |
消除了併發性的可能 |
volatile |
允許併發 |
每次訪問變量,就進行稀有專用內存與對 應之主內存的一致化 |
6. 在單一操作中鎖定所有用到的對象。
7. 以固定而全局性的順序取得多個locks(機鎖)以避免死鎖。
死鎖:當兩個或者多個線程因爲互相等待而阻塞。
8. 優先使用notifyAll()而非notify()。
9. 針對wait()和notifyAll()使用旋鎖(spin locks)。只在代碼等待着某個特定條件,它就應當在一個循環內(或謂旋鎖,spin lock)做那件事(那件事指的是[等待着某個特定條件])。尤其是在判斷null之類的時候,因爲不能夠確保多個線程被喚醒的時候,其條件是否爲空。
10. 使用wait()和notifyAll()替換輪詢循環(polling loops)。
11. 不要對locked object(上鎖對象)之object reference重新賦值。
12. 不要調用stop()或suspend()。
Stop()的本意是用來中止一個線程。中止線程的問題根源不在object locks,而在object 的狀態。當stop()中止一個線程時,會釋放線程持有的所有locks。但是你並不知道當時代碼正在做什麼。
Suspend()的本意時用來[暫時懸掛起一個線程](它由一個對應的resume()函數,用來恢復先前被懸掛起來的線程。Resume()也不再獲得Java 2 SDK 支持)。Suspend()同樣時不安全的,但其原因和stop()的故事不同。和stop()不同,suspend()並不釋放[即將被懸掛支線程說持有的locks]。這些locks 在線程恢復執行前永遠不會釋放。
stop()帶來[攪亂內部數據]的風險,suspend()帶來死鎖的風險。
13. 通過線程(threads)之間的協作來中止線程。
如何中止線程呢?在class 內提供一個變量,以及一個用來設置此變量值的函數。該變量用來表示線程何時應該被中止。
class Thread04 extends Thread {
private volatile boolean stop;
public void stopFlag() {
stop = true;
}
@Override
public void run() {
while (stop) {
super.run();
}
}
}