39曙寡、【JavaSE】【Java 核心類庫(kù)(下)】多線程(2)

1邑退、多線程內(nèi)存情況簡(jiǎn)析

  • 參考:aHR0cHM6Ly93d3cuYmlsaWJpbGkuY29tL3ZpZGVvL0JWMXVKNDExazd3eT9wPTMwOQ==

  • 同樣聲明,對(duì)于 Java 程序運(yùn)行時(shí)的內(nèi)存、JVM 等問(wèn)題换淆,可能闡述的并不會(huì)非常準(zhǔn)確,僅僅是大致上闡述以有助于更好地理解一些復(fù)雜的事情几颜。

  • 代碼一(單線程):

public class MyThread implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }

}
public class MyThreadTest {

    public static void main(String[] args) {
        Thread myThread = new Thread(new MyThread());
        myThread.run(); // 人工直接調(diào)用 run 方法倍试,相當(dāng)于調(diào)用類中的普通的方法
    }

}

此時(shí),內(nèi)存中的情形如下圖:

單線程時(shí)內(nèi)存情況

單線程時(shí)蛋哭,按照代碼的順序县习,main方法在棧中開(kāi)辟空間(入棧),隨后調(diào)用的run方法也同樣在棧中開(kāi)辟空間(入棧)谆趾。CPU 等只需對(duì)這一個(gè)進(jìn)行“操作”(運(yùn)行一個(gè)即可)躁愿。

  • 代碼二(多線程):
多線程模型中棧的圖示

上圖中每個(gè)棧用不同的顏色來(lái)表示。

在多線程的模型中沪蓬,一個(gè)線程需要一個(gè)單獨(dú)的棧區(qū)來(lái)支持彤钟。
當(dāng)調(diào)用start方法的時(shí)候,JVM 會(huì)為該線程開(kāi)辟一個(gè)屬于它的棧區(qū)跷叉,由該棧區(qū)負(fù)責(zé)該線程中涉及到的方法逸雹、變量等营搅。
對(duì)于 CPU 等而言,所要做的就是基于時(shí)間片輪換梆砸、自定義等各類的規(guī)則來(lái)對(duì)每一個(gè)棧區(qū)進(jìn)行交替“操作”(交替“運(yùn)行”每一個(gè)棧區(qū))转质。最終達(dá)到多線程的并發(fā)的目的。

public class MyThread implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
        }
    }

}
public class MyThreadTest {

    public static void main(String[] args) {
        Thread myThread = new Thread(new MyThread());
        myThread.start();
    }

}

此時(shí)帖世,內(nèi)存中的情形如下圖:

多線程時(shí)內(nèi)存情況

補(bǔ)充一點(diǎn):每一個(gè)線程擁有各自的棧區(qū)休蟹,但是堆區(qū)是被所有線程共享的,所以狮暑,從這個(gè)角度出發(fā)鸡挠,需要線程同步機(jī)制來(lái)確保堆區(qū)中的數(shù)據(jù)等正確、安全搬男。

2拣展、關(guān)于創(chuàng)建線程補(bǔ)充

/* 實(shí)現(xiàn) java.lang.Runnable 接口 */

public class MyOperation implements Runnable {

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getId() + ": " + i);
        }
    }

}
public class Main {

    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyOperation());
        Thread thread2 = new Thread(new MyOperation());

        thread1.start();
        thread2.start();
    }

}
public class Main {

    public static void main(String[] args) {
        Runnable runnable = new MyOperation();
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);

        thread1.start();
        thread2.start();
    }

}

好备埃,通過(guò)比較兩個(gè)Main類中的代碼,可以知道要探討的問(wèn)題是:在使用Thread(Runnable runnable)構(gòu)造方法的時(shí)候褐奴,傳入同一個(gè)java.lang.Runnable引用以及傳入不同的java.lang.Runnable的引用按脚,有什么區(qū)別。

首先明確一點(diǎn)敦冬,在上面的兩個(gè)Main類代碼中辅搬,不管傳入的是同一個(gè)java.lang.Runnable引用還是不同的java.lang.Runnable的引用,所創(chuàng)建的線程是兩個(gè)脖旱,不是說(shuō)因?yàn)閭魅氲氖峭粋€(gè)java.lang.Runnable引用而就是一個(gè)線程堪遂,線程的個(gè)數(shù)看的是new Thread(···)的次數(shù)。上面的代碼會(huì)印證這一點(diǎn)萌庆,因?yàn)檩敵龅臅r(shí)候加了Thread.currentThread().getId()溶褪。

下面的問(wèn)題,就是說(shuō)践险,“傳入的引用相同與不同”是否有區(qū)別猿妈?正常情況下是沒(méi)有區(qū)別!因?yàn)閷?shí)現(xiàn)java.lang.Runnable接口的類所定義出的可以理解為是一個(gè)“操作說(shuō)明書(shū)”巍虫,把“操作說(shuō)明書(shū)”交給java.lang.Thread類讓其按照“說(shuō)明書(shū)”的步驟來(lái)執(zhí)行即可彭则。這樣的話,“給兩份一樣的操作說(shuō)明書(shū)”或者“只給同一份的操作說(shuō)明書(shū)”占遥,并沒(méi)有任何區(qū)別俯抖,反倒是“兩份一樣的操作說(shuō)明書(shū)”比較“浪費(fèi)資源”(多new出了一個(gè)java.lang.Runnable類)。

但是筷频,為什么還要在這里說(shuō)討論這個(gè)問(wèn)題蚌成,因?yàn)樵凇熬€程同步”中前痘,如果出現(xiàn)上面的情況,又會(huì)有什么出現(xiàn)問(wèn)題担忧?芹缔!

3、線程同步

3.1瓶盛、概述
  • 之所以要有“線程同步”這樣的一個(gè)機(jī)制最欠,主要是因?yàn)榫€程之間可能會(huì)存在著資源共享的情況,即多個(gè)線程要去使用同一資源(可以表述為“共享資源”惩猫、“臨界資源”等)芝硬,那么就會(huì)出現(xiàn)數(shù)據(jù)不一致、臟數(shù)據(jù)等問(wèn)題轧房。

  • 舉個(gè)例子:
    假設(shè):銀行卡里存有500元拌阴,張三與李四都可以取出這銀行卡里面的錢,張三通過(guò) ATM 取出全部的錢奶镶,而李四想將全部的錢存入手機(jī)的支付軟件的余額中迟赃。巧合的是,兩人差不多在同一時(shí)間進(jìn)行著各自的操作厂镇。李四成功完成了操作纤壁,看到支付軟件上的余額顯示為500元,而此時(shí)捺信,張三這邊也進(jìn)行到 ATM 機(jī)正在“吐錢”酌媒,最終拿到了現(xiàn)金500元。
    當(dāng)然迄靠,現(xiàn)實(shí)情況中是不會(huì)發(fā)生這樣的“好事”的秒咨,否則銀行要關(guān)門了。
    但是梨水,分析一下上面所敘述的過(guò)程拭荤,兩種方式看作兩個(gè)線程茵臭,目的都是“取出同一張銀行卡中的錢”疫诽,但是兩個(gè)線程通過(guò)不同途徑的查詢方式,均得出了“余額有500元”這樣的結(jié)論旦委,然后都成功的取出奇徒。這是一個(gè)非常重大的錯(cuò)誤!

多個(gè)線程并發(fā)讀寫(xiě)同一個(gè)臨界資源時(shí)會(huì)發(fā)生線程并發(fā)安全問(wèn)題缨硝!
線程同步機(jī)制的目的就是為了在多線程并發(fā)條件下摩钙,對(duì)線程之間進(jìn)行通信與協(xié)調(diào),最終能夠正確地處理數(shù)據(jù)等查辩。

下面用代碼模擬一下上面所講的這樣的一個(gè)過(guò)程:

/* 銀行賬戶類胖笛,balance 表示賬戶中的余額 */

public class Balance {

    private int balance;

    public Balance() {
    }

    public Balance(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

}
/* 定義一個(gè)“取款”的操作 */

public class GetBalanceOperation implements Runnable {

    private Balance balance; // 取款的目標(biāo)賬戶
    private int account; // 取款金額

    public GetBalanceOperation(Balance balance, int account) {
        this.balance = balance;
        this.account = account;
    }

    @Override
    public void run() {
        if (balance != null) {
            int temp = balance.getBalance();
            if (temp >= account && account > 0) {
                temp -= account;
                System.out.println("交易處理中···");
                try {
                    Thread.sleep(300); // 一方面為了模擬真實(shí)的取款過(guò)程网持,另一方面為了使多線程所會(huì)引發(fā)的問(wèn)題更明顯
                    balance.setBalance(temp);
                    System.out.println("交易完成!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                System.out.println("余額不足长踊!");
            }
        }
    }

}
public class Main {

    public static void main(String[] args) {
        Balance balance = new Balance(500);
        Runnable operation = new GetBalanceOperation(balance, 600);

        Thread thread1 = new Thread(operation);
        Thread thread2 = new Thread(operation);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
            System.out.println("余額:" + balance.getBalance());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

最后功舀,輸出的結(jié)果是“300”,而現(xiàn)實(shí)生活中身弊,兩次取款后辟汰,最后的余額應(yīng)該是“500-200-200=100”。

為了使“多個(gè)線程并發(fā)對(duì)同一個(gè)臨界資源所會(huì)產(chǎn)生的問(wèn)題”體現(xiàn)的更明顯阱佛,上面的代碼中帖汞,特別是run方法的寫(xiě)法是稍微有些講究的,比如說(shuō)不直接在成員變量balance上進(jìn)行減法凑术,setBalance方法調(diào)用位置在sleep方法后等翩蘸。

線程1與線程2并發(fā)執(zhí)行,由于線程1是先啟動(dòng)的淮逊,當(dāng)線程1執(zhí)行到sleep的時(shí)候鹿鳖,此時(shí)“取款后更新余額”的方法setBalance方法并沒(méi)有調(diào)用,而這時(shí)壮莹,線程2雖然比線程1略晚一點(diǎn)翅帜,但run方法已經(jīng)是在執(zhí)行中了,問(wèn)題就出現(xiàn)了命满,線程1由于還沒(méi)及時(shí)“更新余額”所以線程2讀取到的“余額”仍然是“最初的余額”(實(shí)例代碼自中給的是500)涝滴,所以線程2是基于“最初的余額”進(jìn)行“取款”的。最后的局面是胶台,兩個(gè)線程中的temp值都是“500-200=300”歼疮,所以,就出現(xiàn)了輸出的結(jié)果是“300”這樣的局面诈唬。(總結(jié):線程一執(zhí)行取款時(shí)還沒(méi)來(lái)得及將取款后的余額更新韩脏,線程二就已經(jīng)開(kāi)始取款)

3.2、線程同步機(jī)制(如何實(shí)現(xiàn)線程同步)
  • 繼續(xù)用上面的“余額”代碼來(lái)探討铸磅,首先赡矢,來(lái)看一下用下面的代碼來(lái)解決“余額不正確”的問(wèn)題:
public class Main {

    public static void main(String[] args) {
        Balance balance = new Balance(500);
        Runnable operation = new GetBalanceOperation(balance, 200);

        Thread thread1 = new Thread(operation);
        Thread thread2 = new Thread(operation);

        thread1.start();

        try {
            thread1.join();
            thread2.start(); // thread2 的 start 的調(diào)用位置
            thread2.join();
            System.out.println("余額:" + balance.getBalance());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

從代碼中也能明確地看出解決方案,就是先讓子線程1啟動(dòng)但不讓子線程2啟動(dòng)阅仔,同時(shí)讓主線程等待子線程1完成后再繼續(xù)吹散,因?yàn)樽泳€程2的啟動(dòng)仍然要在主線程中,所以代碼這樣一寫(xiě)八酒,就能夠解決問(wèn)題空民。

但是,這并不是線程同步機(jī)制羞迷,這樣的做法界轩,不是不可以画饥,但是這樣的做法,是一種“用著多線程的語(yǔ)法浊猾,寫(xiě)著單線程思路的代碼”荒澡,意思就是線程的調(diào)度順序還是由代碼的編寫(xiě)順序控制的,我們多線程的目標(biāo)是与殃,在指定我們?cè)撝付ǖ恼{(diào)度規(guī)則后单山,線程的調(diào)度由系統(tǒng)自行完成,而不是主觀通過(guò)控制代碼編寫(xiě)順序?qū)崿F(xiàn)所謂的“多線程”幅疼,如果是這樣米奸,上面的例子中,兩個(gè)線程的代碼還好編寫(xiě)爽篷,那如果是高訪問(wèn)量悴晰、高讀寫(xiě)等特點(diǎn)的大型系統(tǒng),那又如何去編寫(xiě)逐工!

3.2.1铡溪、使用 synchronized 關(guān)鍵字實(shí)現(xiàn)線程同步
  • 使用synchronized關(guān)鍵字來(lái)實(shí)現(xiàn)“同步鎖”(有的地方稱“對(duì)象鎖”、“同步監(jiān)視器”等)機(jī)制從而保證線程在某一執(zhí)行階段的原子性泪喊。

  • 原子性棕硫,所謂原子性是指不可分割的一系列操作指令,在執(zhí)行完畢前不會(huì)被任何其他操作中斷袒啼,要么全部執(zhí)行哈扮,要么全部不執(zhí)行。

  • “鎖”蚓再,這個(gè)字在多線程體系中會(huì)經(jīng)常見(jiàn)到滑肉。多線程場(chǎng)景下,會(huì)出現(xiàn)資源競(jìng)爭(zhēng)等摘仅,需要對(duì)部分代碼等進(jìn)行“加鎖”靶庙,進(jìn)而達(dá)到某個(gè)線程可以短暫“獨(dú)自占有、使用娃属、操作”的相應(yīng)的資源這一目的六荒。

3.2.1.1、synchronized 代碼塊

  • 使用synchronized修飾代碼塊(也稱“同步代碼塊”)膳犹,即表示線程可以對(duì)這部分代碼“加鎖”恬吕∏┰颍“加鎖”意味著線程在執(zhí)行這部分代碼的時(shí)候具有原子性须床,也就是說(shuō),要執(zhí)行就必須執(zhí)行完渐裂,中間不能被打斷(某一線程執(zhí)行“加鎖”的代碼時(shí)豺旬,其他的線程是不可能再同時(shí)執(zhí)行這段被“加鎖”的代碼钠惩,其他線程將會(huì)被變?yōu)椤?strong>阻塞”狀態(tài))。

  • 語(yǔ)法格式如下:

synchronized(類類型的引用) {
    編寫(xiě)所有需要鎖定的代碼族阅;
} 
  • 下面篓跛,使用synchronized代碼塊來(lái)解決“多線程取款”問(wèn)題:
/* 銀行賬戶類,表示一個(gè)銀行賬戶坦刀,balance 為賬戶中的金額 */

public class Balance {

    private int balance;

    public Balance() {
    }

    public Balance(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

}
/* 定義一個(gè)類愧沟,由該類的實(shí)例將來(lái)作為“鎖” */

public class GetBalanceLock {
}
public class GetBalanceOperation implements Runnable {

    private Balance balance;
    private int account;

    private GetBalanceLock lock = new GetBalanceLock(); // “鎖”

    public GetBalanceOperation(Balance balance, int account) {
        this.balance = balance;
        this.account = account;
    }

    @Override
    public void run() {
        synchronized (lock) {
            if (balance != null) {
                int temp = balance.getBalance();
                if (temp >= account && account > 0) {
                    temp -= account;
                    System.out.println("交易處理中···");
                    try {
                        Thread.sleep(300);
                        balance.setBalance(temp);
                        System.out.println("交易完成!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("余額不足鲤遥!");
                }
            }
        }
    }

}
public class Main {

    public static void main(String[] args) {
        Balance balance = new Balance(500);
        Runnable operation = new GetBalanceOperation(balance, 200);

        Thread thread1 = new Thread(operation);
        Thread thread2 = new Thread(operation);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
            System.out.println("余額:" + balance.getBalance());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

交易處理中···
交易完成沐寺!
交易處理中···
交易完成!
余額:100

  • 下面討論一下synchronized括號(hào)中的“鎖”盖奈,“鎖”的要求是混坞,如果希望多個(gè)線程在執(zhí)行到synchronized代碼塊的時(shí)候能夠?qū)崿F(xiàn)“臨時(shí)串行”,那么就要求多個(gè)線程都用的是同一個(gè)“鎖”對(duì)象钢坦,也就是這個(gè)過(guò)程中必須保證“鎖”對(duì)象的唯一性究孕。

  • 可以將synchronized代碼塊想象成“衣服店里的試衣間”,而“鎖”對(duì)應(yīng)的就是“試衣間門上的鎖”爹凹。
    “衣服店里的試衣間”要是沒(méi)有人的話是一直開(kāi)放的厨诸。顧客想試穿衣服,如果試衣間沒(méi)有人禾酱,可以直接進(jìn)去使用泳猬,然后從門的內(nèi)側(cè)把門鎖上,這樣試衣間就暫時(shí)歸這位顧客使用宇植、其他想試穿的顧客就無(wú)法進(jìn)入了得封;等這位顧客試穿完后,將試衣間的門鎖重新從里面打開(kāi)指郁,其他顧客才能進(jìn)去試穿忙上。

某個(gè)線程獲取到 CPU 等資源(想要執(zhí)行synchronized代碼塊的大前提)并要開(kāi)始執(zhí)行synchronized代碼塊中的內(nèi)容時(shí):首先需要“判斷鎖”(試衣間是否有人);可行的話再去“擁有(占有)鎖”(如果試衣間沒(méi)有人闲坎,可以進(jìn)入試衣間疫粥,從門內(nèi)鎖門);synchronized代碼塊執(zhí)行完畢后腰懂,“釋放鎖”(從試衣間開(kāi)門出來(lái))梗逮。

判斷鎖(“判斷試衣間是否有人”可以表述為“判斷鎖”)
-->
獲取鎖(“從門內(nèi)將試衣間鎖上”可以表述為“獲取、占有绣溜、擁有鎖”)
-->
釋放鎖(“試穿完后從試衣間開(kāi)鎖出來(lái)”可以表述為“釋放鎖”)

synchronized 關(guān)鍵字
  • 如果不能在過(guò)程中保證上面所提到的“唯一性”的話慷彤,導(dǎo)致多個(gè)線程在執(zhí)行synchronized語(yǔ)句塊的時(shí)候不會(huì)再像“多個(gè)顧客用一個(gè)試衣間試穿衣服”那樣“串行”燕刻,synchronized語(yǔ)句塊便是無(wú)意義的存在错负。下面看看具體的案例:
public class GetBalanceOperation implements Runnable {

    private Balance balance;
    private int account;

    public GetBalanceOperation(Balance balance, int account) {
        this.balance = balance;
        this.account = account;
    }

    @Override
    public void run() {
        // synchronized 中使用 new
        synchronized (new GetBalanceLock()) {
            if (balance != null) {
                int temp = balance.getBalance();
                if (temp >= account && account > 0) {
                    temp -= account;
                    System.out.println("交易處理中···");
                    try {
                        Thread.sleep(300);
                        balance.setBalance(temp);
                        System.out.println("交易完成失乾!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("余額不足隧枫!");
                }
            }
        }
    }

}
public class Main {

    public static void main(String[] args) {
        Balance balance = new Balance(500);
        Runnable operation = new GetBalanceOperation(balance, 200); // 兩個(gè)線程“共用一份操作說(shuō)明書(shū)”

        Thread thread1 = new Thread(operation);
        Thread thread2 = new Thread(operation);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
            System.out.println("余額:" + balance.getBalance());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

交易處理中···
交易處理中···
交易完成!
交易完成跋选!
余額:300

輸出的結(jié)果很明顯涕癣,并沒(méi)有實(shí)現(xiàn)“線程同步”。其原因就是違反了之前提到的“唯一性”前标。用synchronized (new GetBalanceLock())意味著任何一個(gè)線程執(zhí)行到這里的時(shí)候都會(huì)創(chuàng)建新的“鎖”對(duì)象坠韩,進(jìn)而讓線程“認(rèn)為試衣間里沒(méi)人”,然后“進(jìn)入試衣間”炼列,這樣的話“試衣間”沒(méi)有任何存在的意義了同眯。


public class GetBalanceOperation implements Runnable {

    private Balance balance;
    private int account;

    private GetBalanceLock lock = new GetBalanceLock();

    public GetBalanceOperation(Balance balance, int account) {
        this.balance = balance;
        this.account = account;
    }

    @Override
    public void run() {
        synchronized (lock) {
            if (balance != null) {
                int temp = balance.getBalance();
                if (temp >= account && account > 0) {
                    temp -= account;
                    System.out.println("交易處理中···");
                    try {
                        Thread.sleep(300);
                        balance.setBalance(temp);
                        System.out.println("交易完成!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("余額不足唯鸭!");
                }
            }
        }
    }

}
public class Main {

    public static void main(String[] args) {
        Balance balance = new Balance(500);
        
        // new 出了兩個(gè)相同的操作
        Runnable operation1 = new GetBalanceOperation(balance, 200);
        Runnable operation2 = new GetBalanceOperation(balance, 200);

        Thread thread1 = new Thread(operation1);
        Thread thread2 = new Thread(operation2);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
            System.out.println("余額:" + balance.getBalance());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

交易處理中···
交易處理中···
交易完成须蜗!
交易完成!
余額:300

輸出的結(jié)果很明顯目溉,也沒(méi)有實(shí)現(xiàn)“線程同步”明肮。其原因也是違反了之前提到的“唯一性”。兩個(gè)線程一人一份“操作說(shuō)明書(shū)”缭付。new出來(lái)兩個(gè)“操作說(shuō)明書(shū)”對(duì)象柿估,但是對(duì)應(yīng)的GetBalanceLock“鎖”就有兩個(gè),在執(zhí)行synchronized代碼塊時(shí)也一定不會(huì)是“串行”陷猫。

這種情況下秫舌,可以將“鎖”設(shè)為靜態(tài)即用static修飾,不管new多少份一樣的“操作說(shuō)明書(shū)”绣檬,“鎖”就只有一個(gè)足陨,滿足“唯一性”。

public class GetBalanceOperation implements Runnable {

    private Balance balance;
    private int account;

    private static GetBalanceLock lock = new GetBalanceLock(); // 用 static 修飾

    public GetBalanceOperation(Balance balance, int account) {
        this.balance = balance;
        this.account = account;
    }

    @Override
    public void run() {
        synchronized (lock) {
            if (balance != null) {
                int temp = balance.getBalance();
                if (temp >= account && account > 0) {
                    temp -= account;
                    System.out.println("交易處理中···");
                    try {
                        Thread.sleep(300);
                        balance.setBalance(temp);
                        System.out.println("交易完成娇未!");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("余額不足墨缘!");
                }
            }
        }
    }

}
  • 上述,我們將synchronized代碼塊比作“試衣間”零抬,但也不希望大家誤解一個(gè)事情镊讼,就是synchronized所處理的代碼必須是一模一樣的。因?yàn)樯厦嫠o出的代碼例子所有線程執(zhí)行的代碼是一樣的平夜,“取款”蝶棋,就好像“試衣間”只能“試穿衣服”。
    我們使用各種手段實(shí)現(xiàn)線程同步忽妒,所希望的是資源的安全性玩裙,像使用synchronized代碼塊“包裹”代碼兼贸,根本原因是“這段代碼會(huì)涉及到共享資源(共享的變量等)使用,需要對(duì)線程加以控制献酗,防止共享資源出現(xiàn)安全問(wèn)題(變量讀寫(xiě)‘錯(cuò)亂’等)”寝受。
public class MyLock {
}
/* 通過(guò)這里的代碼坷牛,想傳達(dá)出 synchronized 代碼塊中的代碼可以代表的是不同的任務(wù) */

/* 不要受前面的代碼的影響罕偎,認(rèn)為 synchronized 代碼塊中的代碼只能做同樣的任務(wù) */

/* 未來(lái)可能會(huì)遇到,執(zhí)行的代碼不同京闰,但是代碼所要涉及的資源是同樣的颜及,這個(gè)時(shí)候仍需要合適的線程同步機(jī)制解決 */

public class OperationOne implements Runnable {

    private final MyLock lock;

    public OperationOne(MyLock lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            for (int i = 0; i < 5; i++) {
                System.out.println("CHN");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}
public class OperationTwo implements Runnable {

    private final MyLock lock;

    public OperationTwo(MyLock lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            for (int i = 0; i < 5; i++) {
                System.out.println("PRC");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}
public class Main {

    public static void main(String[] args) {
        MyLock lock = new MyLock();

        Thread thread1 = new Thread(new OperationOne(lock));
        Thread thread2 = new Thread(new OperationTwo(lock));

        thread1.start();
        thread2.start();
    }

}

輸出結(jié)果:先打印5個(gè)“CHN”,再打印5個(gè)“PRC”蹂楣。
因?yàn)閮蓚€(gè)線程用的“鎖”對(duì)象是同一個(gè)俏站,所以執(zhí)行synchronized代碼塊時(shí),線程串行執(zhí)行

  • “鎖”的引用建議用final修飾痊土,防止中途被修改肄扎。非final的對(duì)象可以被重新賦值,“鎖”對(duì)象就不受管控了赁酝。當(dāng)一個(gè)“鎖”被其他線程占有時(shí)犯祠,當(dāng)前線程可以對(duì)“鎖”對(duì)象重新賦值(相當(dāng)于從新創(chuàng)建了一個(gè)“鎖”對(duì)象),從而也拿到了運(yùn)行的權(quán)利酌呆。

3.2.1.2衡载、synchronized 方法

  • 可以使用synchronized關(guān)鍵字來(lái)修飾方法(也稱“同步方法”),所達(dá)到的效果與synchronized代碼塊的效果相同隙袁,即意味著整個(gè)方法被“加鎖”痰娱,涉及的代碼范圍更大。

  • synchronized關(guān)鍵字來(lái)修飾方法的時(shí)候菩收,對(duì)于靜態(tài)方法梨睁、非靜態(tài)方法會(huì)略有區(qū)別。

  • synchronized關(guān)鍵字修飾非靜態(tài)方法娜饵,先看下面的代碼:

/* 銀行賬戶類而姐,balance 表示余額 */

public class Balance {

    private int balance;

    public Balance(int balance) {
        this.balance = balance;
    }

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

}
/* “取款”操作 */

public class GetBalanceOperation implements Runnable {

    private Balance balance;
    private int account;

    public GetBalanceOperation(Balance balance, int account) {
        this.balance = balance;
        this.account = account;
    }

    // synchronized 修飾 run 方法是可以的!;馈拴念!
    // synchronized 修飾 run 方法即表示 run 方法是一個(gè)“同步方法”
    // 即當(dāng)多個(gè)線程執(zhí)行 run 方法中所有的代碼,將會(huì)“串行”執(zhí)行褐缠。某個(gè)線程要將 run 方法全部執(zhí)行完后政鼠,其他線程才能執(zhí)行 run 方法
    @Override
    public synchronized void run() {
        if (balance != null) {
            int temp = balance.getBalance();
            if (account > 0 && account <= temp) {
                temp -= account;
                System.out.println("交易處理中···");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance.setBalance(temp);
                System.out.println("交易完成!");
            }
        }
    }

}
public class Main {

    public static void main(String[] args) {
        Balance balance = new Balance(800);

        GetBalanceOperation getBalanceOperation = new GetBalanceOperation(balance, 200);

        Thread thread1 = new Thread(getBalanceOperation);
        Thread thread2 = new Thread(getBalanceOperation);

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("余額:" + balance.getBalance());
    }

}

看完上面的代碼队魏,大家可能有疑問(wèn)公般,在synchronized代碼塊中“費(fèi)了九牛二虎之力”所講的“鎖”對(duì)象到哪里去了万搔?在用synchronized修飾非靜態(tài)方法時(shí),這個(gè)“鎖”對(duì)象仍是存在的官帘,只不過(guò)這個(gè)“鎖”對(duì)象就是“調(diào)用該非靜態(tài)方法的對(duì)象本身”瞬雹。通過(guò)下面的兩個(gè)等價(jià)代碼,就能理解為什么這么說(shuō):

    @Override
    public synchronized void run() {
        if (balance != null) {
            int temp = balance.getBalance();
            if (account > 0 && account <= temp) {
                temp -= account;
                System.out.println("交易處理中···");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance.setBalance(temp);
                System.out.println("交易完成刽虹!");
            }
        }
    }
    @Override
    public void run() {
        synchronized (this) {
            if (balance != null) {
                int temp = balance.getBalance();
                if (account > 0 && account <= temp) {
                    temp -= account;
                    System.out.println("交易處理中···");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    balance.setBalance(temp);
                    System.out.println("交易完成酗捌!");
                }
            }
        }
    }

synchronized修飾非靜態(tài)方法,等價(jià)于使用synchronized (this)代碼塊然后代碼塊中是整個(gè)方法體涌哲∨昼停“鎖”對(duì)象的引用就是synchronized (this)中的this

就上面的“取款”代碼而言,“鎖”對(duì)象是main方法中定義的GetBalanceOperation類的對(duì)象:getBalanceOperation阀圾。

因?yàn)?code>synchronized修飾的是run方法哪廓,而前面提到這種情況下“鎖”對(duì)象就是“調(diào)用該非靜態(tài)方法的對(duì)象本身,那么是誰(shuí)調(diào)用的run方法初烘?如果大家對(duì)java.lang.Thread類的源碼有有印象的話涡真,對(duì)于使用Thread(Runnable target)構(gòu)造方法創(chuàng)建的線程來(lái)說(shuō),java.lang.Thread對(duì)象使用start啟動(dòng)線程后肾筐,由 JVM 去調(diào)用java.lang.Thread對(duì)象中的run方法哆料,此時(shí)的run方法,本質(zhì)上是由所傳參數(shù)target中的run方法局齿。

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

理清頭緒之后剧劝,再來(lái)看看,這里的“鎖”對(duì)象是否是“唯一”的抓歼,很顯然是“唯一”的讥此。所以線程能夠?qū)崿F(xiàn)同步。

  • 補(bǔ)充代碼:
public class GetBalanceOperation implements Runnable {

    private Balance balance;
    private int account;

    public GetBalanceOperation(Balance balance, int account) {
        this.balance = balance;
        this.account = account;
    }

    // synchronized “間接加修飾 run 方法”
    // 這樣寫(xiě)的話谣妻,相對(duì)來(lái)說(shuō)萄喳,代碼結(jié)構(gòu)會(huì)比較清晰
    private synchronized void get() {
        if (balance != null) {
            int temp = balance.getBalance();
            if (account > 0 && account <= temp) {
                temp -= account;
                System.out.println("交易處理中···");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance.setBalance(temp);
                System.out.println("交易完成!");
            }
        }
    }

    @Override
    public void run() {
        get();
    }

}
  • synchronized關(guān)鍵字修飾靜態(tài)方法蹋半,先看下面的代碼:
public class Product {

    private static int count;

    public static void setCount(int count) {
        Product.count = count;
    }

    public static int getCount() {
        return count;
    }

}
public class BuyThread extends Thread {

    private synchronized static void buy() {
        int temp = Product.getCount();
        if (temp >= 10) {
            System.out.println("正在出貨···");
            temp -= 10;
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Product.setCount(temp);
            System.out.println("出貨成功他巨!");
        }
    }

    @Override
    public void run() {
        buy();
    }

}
public class Main {

    public static void main(String[] args) {
        Product.setCount(30);

        Thread thread1 = new BuyThread();
        Thread thread2 = new BuyThread();

        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("剩余:" + Product.getCount());

    }

}

正在出貨···
出貨成功!
正在出貨···
出貨成功减江!
剩余:10

首先染突,上面的代碼可能與之前的代碼有所不同,主要是因?yàn)橐獎(jiǎng)?chuàng)造能夠展現(xiàn)“使用synchronized修飾靜態(tài)方法”的條件辈灼。

public synchronized static xxx xxx() {···}

對(duì)于通過(guò)“繼承java.lang.Thread類創(chuàng)建線程類份企,然后再去使用new創(chuàng)建多個(gè)線程”的方式,如果需要進(jìn)行線程同步的話巡莹,那必須確彼局荆“鎖”對(duì)象是唯一的甜紫。之前可能大家看到,面對(duì)這樣的情景骂远,可以使用synchronized代碼塊然后使用static修飾的一個(gè)對(duì)象作為唯一的“鎖”對(duì)象囚霸,實(shí)現(xiàn)線程同步;而在這里所展示的是“使用synchronized修飾靜態(tài)方法”也是能夠面對(duì)這樣相似的情景激才。不過(guò)相對(duì)來(lái)說(shuō)拓型,這種“使用synchronized修飾靜態(tài)方法”相對(duì)來(lái)說(shuō)還是有些局限性的,畢竟在靜態(tài)方法中只能使用靜態(tài)的贸营。

下面的討論一下吨述,為什么“使用synchronized修飾靜態(tài)方法”也能實(shí)現(xiàn)線程同步岩睁?還是同樣的道理钞脂,這個(gè)唯一的“鎖”對(duì)象是什么?這里的“鎖”對(duì)象是類對(duì)象捕儒,每個(gè)類都有唯一的一個(gè)類對(duì)象冰啃,獲取類對(duì)象:類名.class

類對(duì)象:在“面向?qū)ο缶幊獭敝辛跤ǎ叭f(wàn)物皆對(duì)象”理念是一直貫穿始終的阎毅,所有的類,包括 Java 本身提供的類点弯、由我們自定義的類等最終都會(huì)歸為一個(gè)“類”扇调,所有的類都是這個(gè)“類”的對(duì)象。
可以這樣理解抢肛,為什么 JVM 能夠識(shí)別帶class關(guān)鍵字的就能判定它是一個(gè)類狼钮?說(shuō)明在底層中,所有的類有一個(gè)“模板”捡絮,通過(guò)這個(gè)“模板”會(huì)很好的判定哪些是類熬芜,哪些是其他的。而 Java 中“模版”不就可以說(shuō)成類嗎8N取(粗淺理解涎拉,詳細(xì)內(nèi)容,見(jiàn)TODO 反射機(jī)制

這樣的話的圆,上面的代碼可以等價(jià)于:

public class BuyThread extends Thread {

    private static void buy() {
        synchronized (BuyThread.class) {
            int temp = Product.getCount();
            if (temp >= 10) {
                System.out.println("正在出貨···");
                temp -= 10;
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Product.setCount(temp);
                System.out.println("出貨成功鼓拧!");
            }
        }
    }

    @Override
    public void run() {
        buy();
    }

}

正在出貨···
出貨成功!
正在出貨···
出貨成功越妈!
剩余:10

注意:靜態(tài)方法與非靜態(tài)方法同時(shí)使用了synchronized后它們之間是非互斥關(guān)系的季俩,原因在于靜態(tài)方法的“鎖”對(duì)象是類對(duì)象而非靜態(tài)方法的“鎖”對(duì)象的是當(dāng)前方法所屬對(duì)象。

3.2.1.3叮称、synchronized 注意事項(xiàng)

  • 多個(gè)需要同步的線程在訪問(wèn)同步塊時(shí)(當(dāng)多個(gè)線程因?yàn)橘Y源等問(wèn)題希望“串行”執(zhí)行某些代碼時(shí))种玛,使用的應(yīng)該是同一個(gè)鎖對(duì)象引用藐鹤。

  • 在使用同步塊時(shí)應(yīng)當(dāng)盡量減少同步范圍以提高并發(fā)的執(zhí)行效率,即synchronized關(guān)鍵字影響的范圍盡可能小赂韵,比如“一般情況下娱节,能小范圍地使用synchronized代碼塊的就沒(méi)必要用synchronized修飾整個(gè)方法”。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末祭示,一起剝皮案震驚了整個(gè)濱河市肄满,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌质涛,老刑警劉巖稠歉,帶你破解...
    沈念sama閱讀 218,525評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異汇陆,居然都是意外死亡怒炸,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,203評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門毡代,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)阅羹,“玉大人,你說(shuō)我怎么就攤上這事教寂∧笥悖” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,862評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵酪耕,是天一觀的道長(zhǎng)导梆。 經(jīng)常有香客問(wèn)我,道長(zhǎng)迂烁,這世上最難降的妖魔是什么看尼? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,728評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮婚被,結(jié)果婚禮上狡忙,老公的妹妹穿的比我還像新娘。我一直安慰自己址芯,他們只是感情好灾茁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,743評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著谷炸,像睡著了一般北专。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上旬陡,一...
    開(kāi)封第一講書(shū)人閱讀 51,590評(píng)論 1 305
  • 那天拓颓,我揣著相機(jī)與錄音,去河邊找鬼描孟。 笑死驶睦,一個(gè)胖子當(dāng)著我的面吹牛砰左,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播场航,決...
    沈念sama閱讀 40,330評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼缠导,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了溉痢?” 一聲冷哼從身側(cè)響起僻造,我...
    開(kāi)封第一講書(shū)人閱讀 39,244評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎孩饼,沒(méi)想到半個(gè)月后髓削,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,693評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡镀娶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,885評(píng)論 3 336
  • 正文 我和宋清朗相戀三年立膛,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汽畴。...
    茶點(diǎn)故事閱讀 40,001評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡旧巾,死狀恐怖耸序,靈堂內(nèi)的尸體忽然破棺而出忍些,到底是詐尸還是另有隱情,我是刑警寧澤坎怪,帶...
    沈念sama閱讀 35,723評(píng)論 5 346
  • 正文 年R本政府宣布罢坝,位于F島的核電站,受9級(jí)特大地震影響搅窿,放射性物質(zhì)發(fā)生泄漏嘁酿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,343評(píng)論 3 330
  • 文/蒙蒙 一男应、第九天 我趴在偏房一處隱蔽的房頂上張望闹司。 院中可真熱鬧,春花似錦沐飘、人聲如沸游桩。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,919評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)借卧。三九已至,卻和暖如春筛峭,著一層夾襖步出監(jiān)牢的瞬間铐刘,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,042評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工影晓, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留镰吵,地道東北人檩禾。 一個(gè)月前我還...
    沈念sama閱讀 48,191評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像疤祭,于是被迫代替她去往敵國(guó)和親锌订。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,955評(píng)論 2 355