Java多線程6:synchronized鎖定類方法、volatile關鍵字及其他


同步靜態方法

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即可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM