Java多執行緒/併發13、保持執行緒間的資料獨立: Collections.synchronizedMap應用

唐大麥發表於2017-04-28

現在流行分散式計算,分散式計算就是先分開計算,然後統一彙總。比如這道題目: 這裡寫圖片描述 。先別跑,小學題很簡單的。
解釋一下,左邊那一砣是計算從1加到n的值(求和),右邊是n乘到1的值(階乘),再把兩個值相加得到最終結果。假設求和運算需要5秒鐘,階乘運算需要7秒鐘,相加的運算需要1秒,那麼總耗時是13秒。而在分散式計算中,由兩臺機器同時進行計算,得到求和及階乘的兩個結果只需要7秒,再相加需要1秒,總耗時8秒。

下面,我們通過兩個執行緒來模擬這個過程。

首先定義計算類,這三個類作為外部類,存在於Main()函式所在類的外面。程式碼很簡單,看起來很長,是因為加了幾組try..catch對。

/* 定義計算器,讓計算由統一的類來管理 */
class Calculator {
    public static int result = 0;
     void getFactorial(int n) throws Exception {
        /* 模擬階乘運算需要7秒 */
        try {
            Thread.sleep(7000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            /* 進行階乘運算 */
            result = Factorial.GetResult(n);
        } catch (Exception e) {
            throw new Exception(e.getMessage());
        }
        System.out.println("執行緒" + Thread.currentThread().getName()
                + "輸出結果:" + String.valueOf(Calculator.result));
    }

     void getAccu(int n) throws Exception {
        /* 模擬求和運算需要5秒 */
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            result = Accu.GetResult(n);/* 進行求和運算 */
        } catch (Exception e) {
            throw new Exception(e.getMessage());
        }
        System.out.println("執行緒" + Thread.currentThread().getName()
                + "輸出結果:" + String.valueOf(Calculator.result));
    }
}

/* 定義階乘處理類Factorial 這裡是模擬實現,實際開發中需要用大數處理類BigInteger才行*/
class Factorial {
    public static int GetResult(int n) throws Exception {
        if (n < 0 || n > 10) {
            throw new Exception("error:玩玩而已,輸入不能小於零,也不能大於10");
        }
        if (n <= 2) {
            return n;
        }
        return n * GetResult(n - 1);
    }
}

/* 定義求和處理類Accu */
class Accu {
    public static int GetResult(int n) throws Exception {
        if (n < 0 || n >= 1000) {
            throw new Exception("error:玩玩而已,輸入不能小於零,也不能大於1000");
        }
        if (n <= 1) {
            return n;
        }
        return n + GetResult(n - 1);
    }
}

接下來,在Main()主執行緒中開始呼叫類來計算。

public class ExecuteDemo {
    public static void main(String[] args) {
        final Calculator calculator = new Calculator();
        new Thread(new Runnable() {
            public void run() {
                try {
                    calculator.getFactorial(10);
                } catch (Exception e) {
                    System.err.println(e.getMessage());
                }
                /*模擬很多執行緒併發執行時造成的競爭*/
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Factorial結果:" + String.valueOf(Calculator.result)); 

            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                try {
                    calculator.getAccu(10);
                } catch (Exception e) {
                    System.err.println(e.getMessage());
                }
                /*模擬很多執行緒併發執行時造成的競爭*/
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Accu結果:" + String.valueOf(Calculator.result));  
            }
        }).start();
    }
}

輸出結果:

執行緒Thread-1輸出結果:55
執行緒Thread-0輸出結果:3628800
Accu結果:3628800
Factorial結果:3628800

執行緒呼叫方法運算的值是正確的,但是在取值卻存在錯誤,Thread-1的計算結果被覆蓋了。看來寫出的這個計算類並不是執行緒安全的,問題就出在存放計算結果的變數static int result上。因為靜態成員(static member)作為公共變數,就是放在共享記憶體區域的(方法區)。

在Calculator類中,我們用一個static int result來儲存類中成員方法計算的結果,但多個執行緒併發呼叫方法時,都會搶佔result向其寫入資料,最終只會保留最後一個執行緒計算的值。我們需要讓每個執行緒都保持各自的計算結果,自然想到了HashMap來儲存。java提供了一個執行緒安全的HashMap包裝: Collections.synchronizedMap。
我們來改動一下Calculator類,程式碼如下:

class Calculator {
    public static Map<Thread, Integer> DataContainer = Collections.synchronizedMap(new HashMap<Thread, Integer>());
     void getFactorial(int n) throws Exception {
        /* 模擬階乘運算需要7秒 */
        try {
            Thread.sleep(7000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int result=0;
        try {
            result = Factorial.GetResult(n);/* 進行階乘運算 */
            DataContainer.put(Thread.currentThread(), result);
        } catch (Exception e) {
            throw new Exception(e.getMessage());
        }
        System.out.println("執行緒" + Thread.currentThread().getName()
                + "輸出結果:" + String.valueOf(result));
    }

     void getAccu(int n) throws Exception {
        /* 模擬求和運算需要5秒 */
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int result=0;
        try {
            result = Accu.GetResult(n);/* 進行求和運算 */
            DataContainer.put(Thread.currentThread(), result);
        } catch (Exception e) {
            throw new Exception(e.getMessage());
        }
        System.out.println("執行緒" + Thread.currentThread().getName()
                + "輸出結果:" + String.valueOf(result));
    }
}

Main()函式的輸出調整:

public static void main(String[] args) {
        final Calculator calculator = new Calculator();
        new Thread(new Runnable() {
            public void run() {
                try {
                    calculator.getFactorial(10);
                } catch (Exception e) {
                    System.err.println(e.getMessage());
                }
                /* 模擬耗時操作中多個執行緒執行時搶佔造成的競爭 */
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Factorial結果:"
                        + String.valueOf(Calculator.DataContainer.get(Thread
                                .currentThread())));

            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                try {
                    calculator.getAccu(10);
                } catch (Exception e) {
                    System.err.println(e.getMessage());
                }
                /* 模擬耗時操作中多個執行緒執行時搶佔造成的競 */
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Accu結果:"
                        + String.valueOf(Calculator.DataContainer.get(Thread
                                .currentThread())));
            }
        }).start();
    }

兩個執行緒互不影響,成功輸出:

執行緒Thread-1輸出結果:55
執行緒Thread-0輸出結果:3628800
Accu結果:55
Factorial結果:3628800

最後,計算兩個結果的和
在Main()函式兩個執行緒呼叫的後面,加上下面這段,看看輸出的結果:

Iterator<Map.Entry<Thread, Integer>> it=Calculator.DataContainer.entrySet().iterator();
int sum=0;
while(it.hasNext()){
    Map.Entry<Thread, Integer> entry = (Map.Entry<Thread, Integer>) it.next(); 
    Integer value = (Integer)entry.getValue(); 
    sum+=value;
}
System.out.println("最終計算結果"+String.valueOf(sum));

執行輸出:

最終計算結果0
執行緒Thread-1輸出結果:55
執行緒Thread-0輸出結果:3628800
Accu結果:55
Factorial結果:3628800

我K,還是不對。
原來,進入Main()函式後,開啟兩個子執行緒計算後,主執行緒並沒有停止,繼續往下執行。由於兩個子執行緒是耗時操作,而主執行緒如小李飛刀般快的速度往下執行,這時侯Calculator.DataContainer還是空的,結果當然為0。
我們應該阻塞主執行緒,直到Calculator.DataContainer有兩個計算結果值後,才允許計算最後的相加結果。
Iterator it=Calculator.DataContainer.entrySet().iterator(); 這句的前面加上:

while(Calculator.DataContainer.size()<2){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

還有一種方法,呼叫兩個執行緒的join()方法來實現呼叫執行緒(主執行緒)阻塞,等兩個執行緒執行完畢再繼續執行主執行緒,不過這要求兩個執行緒不能用匿名方式實現,這裡就不改程式了。具體join用法參考《Thread.Join()讓呼叫執行緒等待子執行緒
執行輸出:

執行緒Thread-1輸出結果:55
執行緒Thread-0輸出結果:3628800
最終計算結果3628855
Accu結果:55
Factorial結果:3628800

相關文章