Java多執行緒6:synchronized鎖定類方法、volatile關鍵字及其他

五月的倉頡發表於2015-10-03

同步靜態方法

synchronized還可以應用在靜態方法上,如果這麼寫,則代表的是對當前.java檔案對應的Class類加鎖。看一下例子,注意一下printC()並不是一個靜態方法:

public class ThreadDomain25
{
    public synchronized static void printA()
    {
        try
        {
            System.out.println("執行緒名稱為:" + Thread.currentThread().getName() + 
                    "在" + System.currentTimeMillis() + "進入printA()方法");
            Thread.sleep(3000);
            System.out.println("執行緒名稱為:" + Thread.currentThread().getName() + 
                    "在" + System.currentTimeMillis() + "離開printA()方法");
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
    
    public synchronized static void printB()
    {
        System.out.println("執行緒名稱為:" + Thread.currentThread().getName() + 
                "在" + System.currentTimeMillis() + "進入printB()方法");
        System.out.println("執行緒名稱為:" + Thread.currentThread().getName() + 
                "在" + System.currentTimeMillis() + "離開printB()方法");

    }
    
    public synchronized void printC()
    {
        System.out.println("執行緒名稱為:" + Thread.currentThread().getName() + 
                "在" + System.currentTimeMillis() + "進入printC()方法");
        System.out.println("執行緒名稱為:" + Thread.currentThread().getName() + 
                "在" + System.currentTimeMillis() + "離開printC()方法");
    }
}

寫三個執行緒分別呼叫這三個方法:

public class MyThread25_0 extends Thread
{
    public void run()
    {
        ThreadDomain25.printA();
    }
}
public class MyThread25_1 extends Thread
{
    public void run()
    {
        ThreadDomain25.printB();
    }
}
public class MyThread25_2 extends Thread
{
    private ThreadDomain25 td;
    
    public MyThread25_2(ThreadDomain25 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.printC();
    }
}

寫個main函式啟動這三個執行緒:

public static void main(String[] args)
{
    ThreadDomain25 td = new ThreadDomain25();
    MyThread25_0 mt0 = new MyThread25_0();
    MyThread25_1 mt1 = new MyThread25_1();
    MyThread25_2 mt2 = new MyThread25_2(td);
    mt0.start();
    mt1.start();
    mt2.start();
}

看一下執行結果:

執行緒名稱為:Thread-0在1443857019710進入printA()方法
執行緒名稱為:Thread-2在1443857019710進入printC()方法
執行緒名稱為:Thread-2在1443857019710離開printC()方法
執行緒名稱為:Thread-0在1443857022710離開printA()方法
執行緒名稱為:Thread-1在1443857022710進入printB()方法
執行緒名稱為:Thread-1在1443857022710離開printB()方法

從執行結果來,對printC()方法的呼叫和對printA()方法、printB()方法的呼叫時非同步的,這說明了靜態同步方法和非靜態同步方法持有的是不同的鎖,前者是類鎖,後者是物件鎖

所謂類鎖,舉個再具體的例子。假如一個類中有一個靜態同步方法A,new出了兩個類的例項B和例項C,執行緒D持有例項B,執行緒E持有例項C,只要執行緒D呼叫了A方法,那麼執行緒E呼叫A方法必須等待執行緒D執行完A方法,儘管兩個執行緒持有的是不同的物件。

 

volatile關鍵字

直接先舉一個例子:

public class MyThread28 extends Thread
{
    private boolean isRunning = true;

    public boolean isRunning()
    {
        return isRunning;
    }

    public void setRunning(boolean isRunning)
    {
        this.isRunning = isRunning;
    }
    
    public void run()
    {
        System.out.println("進入run了");
        while (isRunning == true){}
        System.out.println("執行緒被停止了");
    }
}
public static void main(String[] args)
{
    try
    {
        MyThread28 mt = new MyThread28();
        mt.start();
        Thread.sleep(1000);
        mt.setRunning(false);
        System.out.println("已賦值為false");
    }
    catch (InterruptedException e)
    {
        e.printStackTrace();
    }
}

看一下執行結果:

進入run了
已賦值為false

也許這個結果有點奇怪,明明isRunning已經設定為false了, 執行緒還沒停止呢?

這就要從Java記憶體模型(JMM)說起,這裡先簡單講,虛擬機器那塊會詳細講的。根據JMM,Java中有一塊主記憶體,不同的執行緒有自己的工作記憶體,同一個變數值在主記憶體中有一份,如果執行緒用到了這個變數的話,自己的工作記憶體中有一份一模一樣的拷貝。每次進入執行緒從主記憶體中拿到變數值,每次執行完執行緒將變數從工作記憶體同步回主記憶體中。

出現列印結果現象的原因就是主記憶體和工作記憶體中資料的不同步造成的。因為執行run()方法的時候拿到一個主記憶體isRunning的拷貝,而設定isRunning是在main函式中做的,換句話說 ,設定的isRunning設定的是主記憶體中的isRunning,更新了主記憶體的isRunning,執行緒工作記憶體中的isRunning沒有更新,當然一直死迴圈了,因為對於執行緒來說,它的isRunning依然是true。

解決這個問題很簡單,給isRunning關鍵字加上volatile。加上了volatile的意思是,每次讀取isRunning的值的時候,都先從主記憶體中把isRunning同步到執行緒的工作記憶體中,再當前時刻最新的isRunning。看一下給isRunning加了volatile關鍵字的執行效果:

進入run了
已賦值為false
執行緒被停止了

看到這下執行緒停止了,因為從主記憶體中讀取了最新的isRunning值,執行緒工作記憶體中的isRunning變成了false,自然while迴圈就結束了。

volatile的作用就是這樣,被volatile修飾的變數,保證了每次讀取到的都是最新的那個值。執行緒安全圍繞的是可見性原子性這兩個特性展開的,volatile解決的是變數在多個執行緒之間的可見性,但是無法保證原子性

多提一句,synchronized除了保障了原子性外,其實也保障了可見性。因為synchronized無論是同步的方法還是同步的程式碼塊,都會先把主記憶體的資料拷貝到工作記憶體中,同步程式碼塊結束,會把工作記憶體中的資料更新到主記憶體中,這樣主記憶體中的資料一定是最新的。

 

原子類也無法保證執行緒安全

原子操作表示一段操作是不可分割的,沒有其他執行緒能夠中斷或檢查正在原子操作中的變數。一個原子類就是一個原子操作可用的類,它可以在沒有鎖的情況下保證執行緒安全。

但是這種執行緒安全不是絕對的,在有邏輯的情況下輸出結果也具有隨機性,比如

public class ThreadDomain29
{
    public static AtomicInteger aiRef = new AtomicInteger();
    
    public void addNum()
    {
        System.out.println(Thread.currentThread().getName() + "加了100之後的結果:" + 
                aiRef.addAndGet(100));
        aiRef.getAndAdd(1);
    }
}
public class MyThread29 extends Thread
{
    private ThreadDomain29 td;
    
    public MyThread29(ThreadDomain29 td)
    {
        this.td = td;
    }
    
    public void run()
    {
        td.addNum();
    }
}
public static void main(String[] args)
{
    try
    {
        ThreadDomain29 td = new ThreadDomain29();
        MyThread29[] mt = new MyThread29[5];
        for (int i = 0; i < mt.length; i++)
        {
            mt[i] = new MyThread29(td);
        }
        for (int i = 0; i < mt.length; i++)
        {
            mt[i].start();
        }
        Thread.sleep(1000);
        System.out.println(ThreadDomain29.aiRef.get());
    } 
    catch (InterruptedException e)
    {
        e.printStackTrace();
    }
}

這裡用了一個Integer的原子類AtomicInteger,看一下執行結果:

Thread-1加了100之後的結果:200
Thread-4加了100之後的結果:500
Thread-3加了100之後的結果:400
Thread-2加了100之後的結果:300
Thread-0加了100之後的結果:100
505

顯然,結果是正確的,但不是我們想要的,因為我們肯定希望按順序輸出加了之後的結果,現在卻是200、500、400、300、100這麼輸出。導致這個問題產生的原因是aiRef.addAndGet(100)和aiRef.addAndGet(1)這兩個操作是可分割導致的。

解決方案,就是給addNum方法加上synchronized即可。

相關文章