1. 程式人生 > >【Java多執行緒與併發庫】3.傳統執行緒互斥技術

【Java多執行緒與併發庫】3.傳統執行緒互斥技術

執行緒的同步互斥與通訊
互斥的問題在使用執行緒的時候是我們必須要注意的。
例如兩個執行緒同時開啟,由於業務規則,需要訪問同一個物件,要取得該物件
中的資料進行修改。
這樣多個執行緒對同一個資料進行操作的例項有很多,例如銀行交易。我們的賬戶中原來
有2000元,在同一時間,我們刷卡200購物,然後有人給我們轉賬500元,我們的賬戶餘額
應該是2000-200+500=2300元。
但是,在類似上述場景的時候,我們不對執行緒做任何的操作,這個時候我們的資料可能就會出錯。

只要涉及到多個執行緒操作相同資料的時候就會出現執行緒安全的問題。這是我們編寫多執行緒程式的
時候特別需要注意的問題。

接下來我們編寫一個傳統的涉及執行緒安全問題的程式樣例。
我們開啟兩個執行緒,分別列印一個傳入的資料,列印方式是一個一個字元的列印。
package cn.edu.hpu.test;

public class TraditionalThreadSynchronized {

    public static void main(String[] args) {
        new TraditionalThreadSynchronized().init();
    }
    
    public void init(){

        final Outputer outputer=new Outputer();
        
        new Thread(new Runnable(){
            public void run() {
                while(true){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    outputer.output("ABCDEFGHIJKLNOPQRST");
                }
            }
        }    
        ).start();
        
        new Thread(new Runnable(){
            public void run() {
                while(true){
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    outputer.output("abcdefghijklmnopqrst");
                }
            }
        }    
        ).start();
    }
    
    class Outputer{
        public void output(String name){
            int len=name.length();
            for (int i = 0; i < len; i++) {
                System.out.print(name.charAt(i));
            }
            System.out.println();
        }
    }
}

我們執行一下這程式:

發現除了完整列印大寫和小寫字串之外,有些時候列印某個執行緒的字元時,轉而列印其它執行緒的字元。

如果要求每一個執行緒在執行的時候,中途不被打斷,我們就要實現執行緒的“原子性”。即是執行某個執行緒的時候,其它執行緒不能執行。

實現執行緒的“互斥性”,我們要給執行緒中需要獨立執行的程式碼塊或者方法加“執行緒鎖”。

我們使用synchronized來給需要執行緒獨立執行的程式碼來加鎖。其中synchronized有一個引數,該引數要求
是一個執行緒公用的引數,該引數具有唯一性,只要該引數的引用沒有被釋放,其他使用到該引數的執行緒就不能進行。

因為我們所有的執行緒呼叫的都是同一個Outputer類,我們給該類一個引數,將該引數
作為執行緒鎖的“鑰匙”:
class Outputer{
    String key="lock";
    public void output(String name){
        int len=name.length();
        synchronized(key){
            for (int i = 0; i < len; i++) {
                System.out.print(name.charAt(i));
            }
            System.out.println();
        }
    }
}

如果每一個執行緒自己new一個Outputer類,那我們剛剛的那個鎖就失去作用了,因為每個執行緒的Outputer
類的key引數不是同一個了,這個時候我們將鎖的引數改為該類本身:
class Outputer{
    public void output(String name){
        int len=name.length();
        synchronized(this){
            for (int i = 0; i < len; i++) {
                System.out.print(name.charAt(i));
            }
            System.out.println();
        }
    }
}

當然,我們只是想在呼叫Outputer類的output方法的時候實現互斥,合適的操做是給output方法加鎖:
class Outputer{
    public synchronized void output(String name){
        int len=name.length();
        for (int i = 0; i < len; i++) {
            System.out.print(name.charAt(i));
        }
        System.out.println();
    }
}

如果我們頻繁在加鎖的方法裡再加鎖的話,很有可能會發生“死鎖”的現象。

最後,如果有類是靜態類,呼叫的加鎖方法又是靜態方法,該類的其它非靜態方法要與其互斥的話,
非靜態方法中的synchronized(this)要改成synchronized(類名.class)。

最後,獻上傳統互斥技術的總結圖: