Thread synchronized

      在〈Thread synchronized〉中尚無留言

原子作業

原子作業, 是單一個操作, 具有不可被分割及中斷的意思, 就像原子一樣, 是最小的單位.
如下代碼 : a=10, 屬原子作業, 因為組合語言翻譯為 mov a, 10, 一個指令就可以完成.
但如果是 i++, 則不屬原子作業, 組合語言會翻譯成3個指令如下

mov eax,【xxxxxxxx】
inc eax
mov 【xxxxxxxx】, eax

即然被編譯成三個指令, 就有可能中途被中斷. 所以不是原子作業

組合語言並沒有原子作業

組合語言其實並沒有原子作業, 因為你下達什麼指令, 它就作什麼事, 笨的很. 原子作業是一連串的演算法所規範, 由C語言來規定的.

C語言也沒有原子作業

C語言是所有語言的鼻祖, 但C其實也沒有原子作業

以往的C 標準中並沒有對原子操作進行規定, 通常是藉助第三方的執行緒庫,如intel的pthread來實現. 但從C11開始, 引入原子操作的概念, 標頭檔提供多種原子運算元數據型別, 如atomic_bool,atomic_int等. 如果我們在多個執行緒中, 共享這些型別資料, 編譯器將保證這些指令都是原子作業. 也就是說, 確保任意時刻只有一個執行緒能對這個資源進行訪問. 編譯器保證, 多個執行緒訪問這個共享資源絕對正確.

Java原子作業

二個或以上的執行緒, 存取物件變數時, 情況就會變的很複雜.

Java使用synchronized 實作了C11的pthread函數庫, 確保synchrnoized區塊內的程式碼, 在執行完成前, 不被其他執行緒所干擾.

為了保證某一段程式碼能在目前執行緒完成作業前, 不會被其他執行緒重複執行, 這就是同步作業(Synchronized)

volatile [ˋvɑlət!]

在變數前加上volatile修飾子, 如
volatile int a=10;

此即告訴JVM, 此變數是不穩定的, 所以當線程訪問時, 就會強迫從共用記憶体區重新取得該變數的值, 讓所有的線程看到的值都是同一個, 所以使用volatile會很耗費資源
但此修飾子只保証執行緒寫回記憶体這段是同步, 但不保証只有一個執行緒在存取這個值, 所以仍有其風險

synchronized區塊

先看下面的程式碼, run中的迴圈, 有可能 t1在執行printf後被退出, 結果tableNo還沒加1, 緊接著t2進來了, 抓到的tableNo就是未加1的值, 所以重複列印相同的數字

public class JavaApp{
    public static void main(String[] args) {
        CleanTable job=new CleanTable();
        Thread t1=new Thread(job,"A");
        Thread t2=new Thread(job,"B");
        t1.start();
        t2.start();
    }
}
class CleanTable implements Runnable{
    int tableNo;
    @Override
    public void run() {
        while(tableNo<1000){
            System.out.printf("%s:%d\n", Thread.currentThread().getName(), tableNo);
            tableNo++;
        }
    }
}

為了修正上面的錯誤, 可於迴圈中加入synchronized, 如此可以保證列印及tableNo加1, 會一次性完成

public class JavaApp{
    public static void main(String[] args) {
        CleanTable job=new CleanTable();
        Thread t1=new Thread(job,"A");
        Thread t2=new Thread(job,"B");
        t1.start();
        t2.start();
    }
}
class CleanTable implements Runnable{
    int tableNo;
    @Override
    public void run() {
        while(tableNo<1000){
            synchronized(this){
                System.out.printf("%s:%d\n", Thread.currentThread().getName(), tableNo);
                tableNo++;
            }
        }
    }
}

請注意上面的跑法

t1進入迴圈, 先取得this的物件鎖, 然後開始列印加1, 但可能在列印時, 就被退出回Runnable區(因為列印是很花時間的), 而且退出後, 是帶著鎖回Runnable區的. 

然後t2進入了, 結果t2拿不到 this的物件鎖, 直接回Runnable

來來回回,反反覆覆, 必需t1加完1且離開synchronized區塊後, 才會將物件鎖歸還, 此時t2 才有機會進入.

物件監控鎖定

每個物件都關連到監控. synchronized 方法監控此物件, static synchronized方法監控此類別, synchronized 區塊必需指定物件的那一個區塊要鎖或不鎖

Synchronized 方法

大部份的人都說, 如果可以使用synchronized區塊的話, 就盡量使用區塊, 把範圍縮小, 才不會讓其他執行緒一直來來回回卻因沒物件鎖不得而入, 浪費CPU資源.

是這樣子沒錯, 但也有些例子又非得使用同步方法不可, 請看如下. t1啟動後, 主執行緒睡個500ms, 再啟動t2, 最主要的原因是要確保t1先跑, 再跑t2.

但t1先跑也沒用, 因為t1要在1000ms 後才會把level改成100. 所以在500ms~1000ms之間, t2啟動了, 抓取到的level還是0

public class JavaApp{
    public static void main(String[] args) {
        Pikachu p=new Pikachu();
        Thread t1=new Thread(new Runnable(){
            @Override
            public void run() {p.setLevel(100);}
        });
        Thread t2=new Thread(new Runnable(){
            @Override
            public void run() {System.out.printf("皮卡丘的等級為 : %d\n", p.getLevel());}
        });
        t1.start();
        
        //底下的sleep, 確保t1先啟動, 再啟動t2
        try {Thread.sleep(500);} catch (InterruptedException ex) {}
        
        t2.start();
    }
}
class Pikachu{
    private int level;
    public void setLevel(int level){
        try {Thread.sleep(1000);}catch (InterruptedException ex) {}
        this.level=level;
    }
    public int getLevel(){
        return level;
    }
}

印出的結果是 - 皮卡丘的等級為 : 0

再看下面的寫法, 在setLevel()及getLevel()都加上synchronized, 此時t1先啟動並取得物件鎖, 然後執行setLevel時, 先去睡個1000ms. t1跑去睡時, 請注意, 是帶著鎖去睡覺的.

所以500ms~1000ms之間, t2啟動了, 要執行getLevel()時卻因為沒有鎖, 所以進不了, 就退出了. 要一直等到t1歸還鑰匙後, t2才有機會進入getLevel(). 所以印出的結果為 100.

其實我們可以下面的程式碼想像成 : 二個房間(方法)共用一把錀匙, 一個人拿了錀匙進入了A房間, 另一個就無法進入B房間. 要等到進入A房的人歸還錀匙後, 另一個才有機會進入B房間.

public class JavaApp{
    public static void main(String[] args) {
        Pikachu p=new Pikachu();
        Thread t1=new Thread(new Runnable(){
            @Override
            public void run() {p.setLevel(100);}
        });
        Thread t2=new Thread(new Runnable(){
            @Override
            public void run() {System.out.printf("皮卡丘的等級為 : %d\n", p.getLevel());}
        });
        t1.start();
        
        //底下的sleep, 確保t1先啟動, 再啟動t2
        try {Thread.sleep(500);} catch (InterruptedException ex) {}
        
        t2.start();
    }
}
class Pikachu{
    private int level;
    public synchronized void setLevel(int level){
        try {Thread.sleep(1000);}catch (InterruptedException ex) {}
        this.level=level;
    }
    public synchronized int getLevel(){
        return level;
    }
}
印出的結果是 - 皮卡丘的等級為 : 100

上述的寫法, 其實只是為了說明synchronized 而以, 事實上, 我們並不會這麼寫. 正確寫法, 請參照如下另一篇文章 Thread join

新Thread與UI元件

SwingUtilities.invokeLater(new Runnable() { 
    public void run() { 
        progressBar.setValue(i); 
    } 
});

 

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *