Thread synchronized

      在〈Thread synchronized〉中尚無留言

原子作業

早期物理學認為原子是構成宇宙萬物的最小單位,無法繼續分割。所以原子作業是指單一個操作,具有不可中斷的意思。

比如 a=10 屬原子作業,因為組合語言會翻譯為

mov a, 10

以上是一個指令,不可能執行到一半就被中斷。但如果是 i++,則不屬原子作業,因為組合語言會翻譯成 3 個指令

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

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

組合語言並沒有原子作業

組合語言其實並沒有原子作業,因為你下達什麼指令它就作什麼事,笨的很。原子作業是一連串的演算法所規範,由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會很耗費資源
但此修飾子只保証執行緒寫回記憶体這段是同步, 但不保証只有一個執行緒在存取這個值, 所以仍有其風險

共同作業與共競

先看下面的代碼

public class J02 {
    public static void main(String[] args) {
        new Thread(new MyJob()).start();
        new Thread(new MyJob()).start();
    }
}
class MyJob implements Runnable{
    int i=0;
    @Override
    public void run() {
        synchronized(this) {
            while(i<100) {
                System.out.println(Thread.currentThread().getName() + " : " +i);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                i+=1;
            }
        }
    }   
}

每個執行緒執行不同的任務,每個任務都有自已的物件變數,這對 Java 而言沒有任何的問題

但如果是多個執行緒執行同一個任務呢? 比如多個人同時要新增修改同一個資料庫。這種方式不是為了加速執行的速度,而是要依現行的狀況有不同的作法,如下所示。

public class J02 {
    public static void main(String[] args) {
        var job=new MyJob();
        new Thread(job).start();
        new Thread(job).start();    
    }
}
class MyJob implements Runnable{
    int i=0;
    @Override
    public void run() {
        synchronized(this) {
            while(i<100) {
                System.out.println(Thread.currentThread().getName() + " : " +i);
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                i+=1;
            }
        }
    }   
}

這種情況就是發生共競 (Race Condition) 的主要原因。什麼是共競呢,上述不同的執行緒會重複相同的數字。共競只有在共同作業時才會發生。

其實「共同作業」這句話不太正確,應該說「共同存取共享資源」,但共同作業的說法比較能理解。

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); 
    } 
});

發佈留言

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