java關(guān)鍵字synchronized的使用

引言

線程安全是并發(fā)編程中的重要關(guān)注點(diǎn),而造成線程安全問題的主要原因有兩點(diǎn):

  1. 存在共享數(shù)據(jù)(臨界資源);
  2. 存在多個(gè)線程共同操作共享數(shù)據(jù);

因此為了解決這個(gè)問題,我們需要這樣的一個(gè)方案,當(dāng)存在多個(gè)線程操作共享數(shù)據(jù)時(shí),需要確保同一時(shí)刻有且只有一個(gè)線程在操作共享數(shù)據(jù),其他線程必須等到該線程處理數(shù)據(jù)完后再進(jìn)行操作,這就是我們所說的互斥鎖,即能達(dá)到互斥訪問目的的鎖。也就是說當(dāng)一個(gè)共享數(shù)據(jù)被當(dāng)前正訪問的線程加上互斥鎖后,在同一時(shí)刻,其他線程只能處于等待的狀態(tài),直到當(dāng)前線程處理完畢釋放該鎖。
在Java中,關(guān)鍵字synchronized可以保證在同一時(shí)刻,只有一個(gè)線程可以執(zhí)行某個(gè)方法或某個(gè)代碼塊。

synchronized的三種使用方式

  • 修飾實(shí)例方法 - 作用于當(dāng)前實(shí)例加鎖,進(jìn)入同步代碼要獲得當(dāng)前實(shí)例的鎖;
  • 修飾靜態(tài)方法 - 作用于當(dāng)前類對(duì)象加鎖,進(jìn)入同步代碼前要獲得當(dāng)前類對(duì)象的鎖;
  • 修飾代碼塊 - 指定加鎖對(duì)象,對(duì)給定對(duì)象加鎖,進(jìn)入同步代碼庫(kù)前要獲得給定對(duì)象的鎖;

synchronized作用于實(shí)例方法

所謂的實(shí)例對(duì)象鎖,指的是用synchronized修飾實(shí)例對(duì)象中的實(shí)例方法,注意這里實(shí)例方法不包括靜態(tài)方法。代碼如下:

public class InstanceSync implements Runnable {

    static int count = 0;

    // synchronized修飾實(shí)例方法
    private synchronized void inc() {
        count++;
    }

    @Override
    public void run() {
        int i = 0;
        while (i++ < 1000) {
            inc();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        InstanceSync instanceSync = new InstanceSync();
        Thread t1 = new Thread(instanceSync);
        Thread t2 = new Thread(instanceSync);

        t1.start();
        t2.start();
        t1.join(); // 等待線程結(jié)束
        t2.join(); // 等待線程結(jié)束

        System.out.println(instanceSync.count);
    }
}

運(yùn)行結(jié)果為:

2000
Process finished with exit code 0

在上述代碼中,我們開啟了二個(gè)線程操作同一個(gè)共享的資源(變量count,由于count++不具備原子性(先讀值,后寫回一個(gè)新值,分二步),因此,我們對(duì)inc方法使用synchronized進(jìn)行修飾,以便保證線程安全。
我們注意到synchronized修飾的是實(shí)例方法inc,在這種情況下,當(dāng)前線程的鎖便是實(shí)例對(duì)象instanceSync,(Java中的線程同步鎖可以是任意對(duì)象)。

如果我們將代碼改成如下:

public class InstanceSync implements Runnable {

    static int count = 0;

    // synchronized修飾實(shí)例方法
    private synchronized void inc() {
        count++;
    }

    @Override
    public void run() {
        int i = 0;
        while (i++ < 1000) {
            inc();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new InstanceSync()); // 重新new一個(gè)實(shí)例
        Thread t2 = new Thread(new InstanceSync()); // 重新new一個(gè)實(shí)例

        t1.start();
        t2.start();
        t1.join(); // 等待線程結(jié)束
        t2.join(); // 等待線程結(jié)束

        System.out.println(count);
    }
}

運(yùn)行結(jié)果發(fā)現(xiàn)是一個(gè)小于2000的值,上述代碼我們犯了一個(gè)錯(cuò)誤,雖然我們使用了synchronized修飾了inc方法,但卻new了兩個(gè)不同的實(shí)例對(duì)象,這就意味著存在兩個(gè)不同的實(shí)例對(duì)象鎖,因此t1和t2都會(huì)進(jìn)入各自的對(duì)象鎖,即t1和t2使用了不同的鎖,所以線程安全無(wú)法保證。
解決這種困境的方法是:將synchronized作用于靜態(tài)的inc方法,這樣的話,對(duì)象鎖就是當(dāng)前類對(duì)象,由于無(wú)論創(chuàng)建多少個(gè)實(shí)例對(duì)象,但對(duì)于類的對(duì)象擁有只有一個(gè),所以這樣的情況下對(duì)象鎖就是唯一的。
下面我們看看如何使用將synchronized作用于靜態(tài)的inc方法上。

synchronized作用于靜態(tài)方法

當(dāng)synchronized作用于靜態(tài)方法時(shí),其鎖就是當(dāng)前類的class對(duì)象鎖。由于靜態(tài)成員不專屬于任何一個(gè)實(shí)例對(duì)象,是類成員,因此通過class對(duì)象鎖可以控制靜態(tài)成員的并發(fā)操作。
注意: 如果線程A調(diào)用一個(gè)實(shí)例對(duì)象的非 static synchronized方法,而線程B需要調(diào)用這個(gè)實(shí)例對(duì)象所屬類的靜態(tài)synchronized方法是被允許的,這不會(huì)發(fā)生互斥現(xiàn)象。原因是訪問靜態(tài)synchronized方法所占用的鎖是當(dāng)前類的class對(duì)象,而訪問非靜態(tài)synchronized方法占用的鎖是當(dāng)前實(shí)例對(duì)象鎖,是兩個(gè)不同的鎖。代碼如下:

public class InstanceSync implements Runnable {

    static int count = 0;

    // 作用于靜態(tài)方法 synchronized修飾實(shí)例方法
    private static synchronized void inc() {
        count++;
    }

    @Override
    public void run() {
        int i = 0;
        while (i++ < 1000) {
            inc();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new InstanceSync()); // 重新new一個(gè)實(shí)例
        Thread t2 = new Thread(new InstanceSync()); // 重新new一個(gè)實(shí)例

        t1.start();
        t2.start();
        t1.join(); // 等待線程結(jié)束
        t2.join(); // 等待線程結(jié)束

        System.out.println(count);
    }
}

我們只是在inc方法前加了static,標(biāo)識(shí)其為靜態(tài)成員方法。運(yùn)行結(jié)果,我們得到了一個(gè)2000的值,這個(gè)是我們預(yù)想到的。

synchronized同步代碼塊

我們除了使用關(guān)鍵字synchronized修飾實(shí)例方法和靜態(tài)方法之外,還可以使用在同步代碼塊上。
在某些情況下,我們編寫的方法體可能比較大,執(zhí)行耗時(shí)比較長(zhǎng),而需要同步的代碼又只有一小部分;如果直接對(duì)整個(gè)方法進(jìn)行同步操作會(huì)發(fā)現(xiàn)得不償失,此時(shí)我們可以使用同步代碼塊的方式對(duì)需要同步的代碼進(jìn)行包裹,同步代碼塊使用如下:

public class InstanceSync implements Runnable {

    private String lockFlag = "lockFlag"; // 定義一個(gè)對(duì)象所

    static int count    = 0;


    @Override
    public void run() {
        // 我這里是一個(gè)大的耗時(shí)操作,不需要同步,無(wú)線程安全
        // ...

        synchronized (lockFlag) {

            int i = 0;
            while (i++ < 1000) {
                count++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(new InstanceSync()); // 重新new一個(gè)實(shí)例
        Thread t2 = new Thread(new InstanceSync()); // 重新new一個(gè)實(shí)例

        t1.start();
        t2.start();
        t1.join(); // 等待線程結(jié)束
        t2.join(); // 等待線程結(jié)束

        System.out.println(count);
    }
}

從上面的代碼看到,我們定義了一個(gè)鎖對(duì)象lockFlag,如果你覺得單獨(dú)再定義一個(gè)鎖對(duì)象麻煩,你也可以這樣:

// this,當(dāng)前實(shí)例對(duì)象鎖
synchronized(this) {
  // ... 實(shí)現(xiàn)部分
}

或者

// class對(duì)象鎖
synchronized(InstanceSync.class) {
  // ... 實(shí)現(xiàn)部分
}

每次當(dāng)線程進(jìn)入synchronized包裹的代碼塊時(shí),就會(huì)要求持有指定的對(duì)象鎖,如果當(dāng)前有其他線程正持有該對(duì)象鎖,那么新到的線程就必須等待,這樣就確保了每次只有一個(gè)線程執(zhí)行count++操作。

擴(kuò)展:synchronized鎖重入

關(guān)鍵字synchronized擁有鎖重入的功能,即在使用synchronized時(shí),當(dāng)一個(gè)線程得到一個(gè)對(duì)象鎖后,再次請(qǐng)求此對(duì)象鎖時(shí)是可以再次得到該對(duì)象鎖的。
這也證明了在一個(gè)synchronized方法/塊的內(nèi)部調(diào)用本類的其他synchronized方法/塊時(shí),是永遠(yuǎn)可以得到鎖的。

我們定義如下三個(gè)類:
Service.java

public class Service {

    public synchronized void service1() {
        System.out.println("--->service1");
        service2();
    }

    public synchronized void service2() {
        System.out.println("--->service2");
        service3();
    }

    public synchronized void service3() {
        System.out.println("--->service3");
        service4();
    }

    public synchronized void service4() {
        System.out.println("--->service4");
    }
}

MyThread.java

public class MyThread extends Thread {

    @Override
    public void run() {
        super.run();
        Service service = new Service();
        service.service1();
    }
}

Run.java

public class Run {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();
    }
}

執(zhí)行代碼,結(jié)果如下:

--->service1
--->service2
--->service3
--->service4

Process finished with exit code 0

可重入鎖 - 即自己可以再次獲取自己的內(nèi)部鎖。比如有一個(gè)線程獲得了某個(gè)對(duì)象的鎖,此時(shí)這個(gè)對(duì)象鎖還沒有釋放,當(dāng)其再次想要獲取這個(gè)對(duì)象的鎖的時(shí)候還是可以獲取的,如果不可鎖重入的話,就會(huì)造成死鎖。
可重入鎖也支持在父子類繼承的環(huán)境中

總結(jié)

對(duì)于synchronized的作用,關(guān)鍵看其持有的鎖對(duì)象,只要你抓住了這個(gè),你就不會(huì)有困擾。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容