Java執行緒:執行緒的同步與鎖

技術小阿哥發表於2017-11-14
一、同步問題提出
 
執行緒的同步是為了防止多個執行緒訪問一個資料物件時,對資料造成的破壞。
例如:兩個執行緒ThreadA、ThreadB都操作同一個物件Foo物件,並修改Foo物件上的資料。
 
public class Foo { 

    private int x = 100; 


    public int getX() { 

        return x; 

    } 


    public int fix(int y) { 

        x = x – y; 

        return x; 

    } 

}
 
public class MyRunnable implements Runnable { 

    private Foo foo = new Foo(); 


    public static void main(String[] args) { 

        MyRunnable r = new MyRunnable(); 

        Thread ta = new Thread(r, “Thread-A”); 

        Thread tb = new Thread(r, “Thread-B”); 

        ta.start(); 

        tb.start(); 

    } 


    public void run() { 

        for (int i = 0; i < 3; i++) { 

            this.fix(30); 

            try { 

                Thread.sleep(1); 

            } catch (InterruptedException e) { 

                e.printStackTrace(); 

            } 

            System.out.println(Thread.currentThread().getName() + ” : 當前foo物件的x值= “ + foo.getX()); 

        } 

    } 


    public int fix(int y) { 

        return foo.fix(y); 

    } 

}
 
執行結果:
Thread-A : 當前foo物件的x值= 40 

Thread-B : 當前foo物件的x值= 40 

Thread-B : 當前foo物件的x值= -20 

Thread-A : 當前foo物件的x值= -50 

Thread-A : 當前foo物件的x值= -80 

Thread-B : 當前foo物件的x值= -80 


Process finished with exit code 0
 
從結果發現,這樣的輸出值明顯是不合理的。原因是兩個執行緒不加控制的訪問Foo物件並修改其資料所致。
 
如果要保持結果的合理性,只需要達到一個目的,就是將對Foo的訪問加以限制,每次只能有一個執行緒在訪問。這樣就能保證Foo物件中資料的合理性了。
 
在具體的Java程式碼中需要完成一下兩個操作:
把競爭訪問的資源類Foo變數x標識為private;
同步哪些修改變數的程式碼,使用synchronized關鍵字同步方法或程式碼。
 
二、同步和鎖定
 
1、鎖的原理
 
Java中每個物件都有一個內建鎖
 
當程式執行到非靜態的synchronized同步方法上時,自動獲得與正在執行程式碼類的當前例項(this例項)有關的鎖。獲得一個物件的鎖也稱為獲取鎖、鎖定物件、在物件上鎖定或在物件上同步。
 
當程式執行到synchronized同步方法或程式碼塊時才該物件鎖才起作用。
 
一個物件只有一個鎖。所以,如果一個執行緒獲得該鎖,就沒有其他執行緒可以獲得鎖,直到第一個執行緒釋放(或返回)鎖。這也意味著任何其他執行緒都不能進入該物件上的synchronized方法或程式碼塊,直到該鎖被釋放。
 
釋放鎖是指持鎖執行緒退出了synchronized同步方法或程式碼塊。
 
關於鎖和同步,有一下幾個要點:
1)、只能同步方法,而不能同步變數和類;
2)、每個物件只有一個鎖;當提到同步時,應該清楚在什麼上同步?也就是說,在哪個物件上同步?
3)、不必同步類中所有的方法,類可以同時擁有同步和非同步方法。
4)、如果兩個執行緒要執行一個類中的synchronized方法,並且兩個執行緒使用相同的例項來呼叫方法,那麼一次只能有一個執行緒能夠執行方法,另一個需要等待,直到鎖被釋放。也就是說:如果一個執行緒在物件上獲得一個鎖,就沒有任何其他執行緒可以進入(該物件的)類中的任何一個同步方法。
5)、如果執行緒擁有同步和非同步方法,則非同步方法可以被多個執行緒自由訪問而不受鎖的限制。

6)、執行緒睡眠時,它所持的任何鎖都不會釋放。

7)、執行緒可以獲得多個鎖。比如,在一個物件的同步方法裡面呼叫另外一個物件的同步方法,則獲取了兩個物件的同步鎖。
8)、同步損害併發性,應該儘可能縮小同步範圍。同步不但可以同步整個方法,還可以同步方法中一部分程式碼塊。
9)、在使用同步程式碼塊時候,應該指定在哪個物件上同步,也就是說要獲取哪個物件的鎖。例如:
    public int fix(int y) {

        synchronized (this) {

            x = x – y;

        }

        return x;

    }
 
當然,同步方法也可以改寫為非同步方法,但功能完全一樣的,例如:
    public synchronized int getX() {

        return x++;

    }
    public int getX() {

        synchronized (this) {

            return x;

        }

    }
效果是完全一樣的。
 
三、靜態方法同步
 
要同步靜態方法,需要一個用於整個類物件的鎖,這個物件是就是這個類(XXX.class)。
例如:
public static synchronized int setName(String name){
      Xxx.name = name;
}
等價於

public static int setName(String name){

      synchronized(Xxx.class){

            Xxx.name = name;

      }

}


 
四、如果執行緒不能不能獲得鎖會怎麼樣
 
如果執行緒試圖進入同步方法,而其鎖已經被佔用,則執行緒在該物件上被阻塞。實質上,執行緒進入該物件的的一種池中,必須在哪裡等待,直到其鎖被釋放,該執行緒再次變為可執行或執行為止。
 
當考慮阻塞時,一定要注意哪個物件正被用於鎖定:
1、呼叫同一個物件中非靜態同步方法的執行緒將彼此阻塞。如果是不同物件,則每個執行緒有自己的物件的鎖,執行緒間彼此互不干預。
 
2、呼叫同一個類中的靜態同步方法的執行緒將彼此阻塞,它們都是鎖定在相同的Class物件上。
 
3、靜態同步方法和非靜態同步方法將永遠不會彼此阻塞,因為靜態方法鎖定在Class物件上,非靜態方法鎖定在該類的物件上。
 
4、對於同步程式碼塊,要看清楚什麼物件已經用於鎖定(synchronized後面括號的內容)。在同一個物件上進行同步的執行緒將彼此阻塞,在不同物件上鎖定的執行緒將永遠不會彼此阻塞。
 
五、何時需要同步
 
在多個執行緒同時訪問互斥(可交換)資料時,應該同步以保護資料,確保兩個執行緒不會同時修改更改它。
 
對於非靜態欄位中可更改的資料,通常使用非靜態方法訪問。
對於靜態欄位中可更改的資料,通常使用靜態方法訪問。
 
如果需要在非靜態方法中使用靜態欄位,或者在靜態欄位中呼叫非靜態方法,問題將變得非常複雜。已經超出SJCP考試範圍了。
 
六、執行緒安全類
 
當一個類已經很好的同步以保護它的資料時,這個類就稱為“執行緒安全的”。
 
即使是執行緒安全類,也應該特別小心,因為操作的執行緒是間仍然不一定安全。
 
舉個形象的例子,比如一個集合是執行緒安全的,有兩個執行緒在操作同一個集合物件,當第一個執行緒查詢集合非空後,刪除集合中所有元素的時候。第二個執行緒也來執行與第一個執行緒相同的操作,也許在第一個執行緒查詢後,第二個執行緒也查詢出集合非空,但是當第一個執行清除後,第二個再執行刪除顯然是不對的,因為此時集合已經為空了。
看個程式碼:
 
public class NameList { 

    private List nameList = Collections.synchronizedList(new LinkedList()); 


    public void add(String name) { 

        nameList.add(name); 

    } 


    public String removeFirst() { 

        if (nameList.size() > 0) { 

            return (String) nameList.remove(0); 

        } else { 

            return null

        } 

    } 

}
 
public class Test { 

    public static void main(String[] args) { 

        final NameList nl = new NameList(); 

        nl.add(“aaa”); 

        class NameDropper extends Thread{ 

            public void run(){ 

                String name = nl.removeFirst(); 

                System.out.println(name); 

            } 

        } 


        Thread t1 = new NameDropper(); 

        Thread t2 = new NameDropper(); 

        t1.start(); 

        t2.start(); 

    } 

}
 
雖然集合物件
    private List nameList = Collections.synchronizedList(new LinkedList());

是同步的,但是程式還不是執行緒安全的。
出現這種事件的原因是,上例中一個執行緒操作列表過程中無法阻止另外一個執行緒對列表的其他操作。
 
解決上面問題的辦法是,在操作集合物件的NameList上面做一個同步。改寫後的程式碼如下:
public class NameList { 

    private List nameList = Collections.synchronizedList(new LinkedList()); 


    public synchronized void add(String name) { 

        nameList.add(name); 

    } 


    public synchronized String removeFirst() { 

        if (nameList.size() > 0) { 

            return (String) nameList.remove(0); 

        } else { 

            return null

        } 

    } 

}
 
這樣,當一個執行緒訪問其中一個同步方法時,其他執行緒只有等待。
 
七、執行緒死鎖
 
死鎖對Java程式來說,是很複雜的,也很難發現問題。當兩個執行緒被阻塞,每個執行緒在等待另一個執行緒時就發生死鎖。
 
還是看一個比較直觀的死鎖例子:
 
public class DeadlockRisk { 

    private static class Resource { 

        public int value; 

    } 


    private Resource resourceA = new Resource(); 

    private Resource resourceB = new Resource(); 


    public int read() { 

        synchronized (resourceA) { 

            synchronized (resourceB) { 

                return resourceB.value + resourceA.value; 

            } 

        } 

    } 


    public void write(int a, int b) { 

        synchronized (resourceB) { 

            synchronized (resourceA) { 

                resourceA.value = a; 

                resourceB.value = b; 

            } 

        } 

    } 

}
 
假設read()方法由一個執行緒啟動,write()方法由另外一個執行緒啟動。讀執行緒將擁有resourceA鎖,寫執行緒將擁有resourceB鎖,兩者都堅持等待的話就出現死鎖。
 
實際上,上面這個例子發生死鎖的概率很小。因為在程式碼內的某個點,CPU必須從讀執行緒切換到寫執行緒,所以,死鎖基本上不能發生。
 
但是,無論程式碼中發生死鎖的概率有多小,一旦發生死鎖,程式就死掉。有一些設計方法能幫助避免死鎖,包括始終按照預定義的順序獲取鎖這一策略。已經超出SCJP的考試範圍。
 
八、執行緒同步小結
 
1、執行緒同步的目的是為了保護多個執行緒反問一個資源時對資源的破壞。
2、執行緒同步方法是通過鎖來實現,每個物件都有切僅有一個鎖,這個鎖與一個特定的物件關聯,執行緒一旦獲取了物件鎖,其他訪問該物件的執行緒就無法再訪問該物件的其他同步方法。
3、對於靜態同步方法,鎖是針對這個類的,鎖物件是該類的Class物件。靜態和非靜態方法的鎖互不干預。一個執行緒獲得鎖,當在一個同步方法中訪問另外物件上的同步方法時,會獲取這兩個物件鎖。
4、對於同步,要時刻清醒在哪個物件上同步,這是關鍵。
5、編寫執行緒安全的類,需要時刻注意對多個執行緒競爭訪問資源的邏輯和安全做出正確的判斷,對“原子”操作做出分析,並保證原子操作期間別的執行緒無法訪問競爭資源。
6、當多個執行緒等待一個物件鎖時,沒有獲取到鎖的執行緒將發生阻塞。
7、死鎖是執行緒間相互等待鎖鎖造成的,在實際中發生的概率非常的小。真讓你寫個死鎖程式,不一定好使,呵呵。但是,一旦程式發生死鎖,程式將死掉。
 
本文轉自 leizhimin 51CTO部落格,原文連結:http://blog.51cto.com/lavasoft/99155,如需轉載請自行聯絡原作者


相關文章