第12章 并發(fā)

第12章 并發(fā)

????????你可能已經(jīng)很熟悉多任務(wù)(multitasking),這是操作系統(tǒng)的一種能力,看起來可以在同一時(shí)刻運(yùn)行多個(gè)程序狈定。例如,在編輯或下載郵件的同時(shí)可以打印文件吱殉。如今掸冤,人們往往都有多 CPU 的計(jì)算機(jī),但是友雳,并發(fā)執(zhí)行的進(jìn)程數(shù)目并不受限于 CPU 數(shù)目稿湿。操作系統(tǒng)會(huì)為每個(gè)進(jìn)程分配 CPU 時(shí)間片,給人并行處理的感覺押赊。

我們可以在一個(gè)或多個(gè) CPU 的操作系統(tǒng)中同時(shí)運(yùn)行多個(gè)進(jìn)程饺藤,每個(gè)進(jìn)程 CPU 會(huì)分配時(shí)間片,給我們的感覺是這些任務(wù)都可以同時(shí)運(yùn)行流礁。

????????多線程程序在更低一層擴(kuò)展了多任務(wù)的概念:?jiǎn)蝹€(gè)程序看起來在同時(shí)完成多個(gè)任務(wù)涕俗。每個(gè)任務(wù)在一個(gè)線程(thread)中執(zhí)行,線程是控制線程的簡(jiǎn)稱神帅。如果一個(gè)程序可以同時(shí)運(yùn)行多個(gè)線程再姑,則稱這個(gè)程序是多線程的(multithreaded)。

什么樣的程序才能稱得上是多線程的找御,只有能控制多個(gè)線程的應(yīng)用程序才可以元镀。

????????那么,多進(jìn)程與多線程有哪些區(qū)別呢霎桅?本質(zhì)的區(qū)別在于每個(gè)進(jìn)程都擁有自己的一整套變量栖疑,而線程則共享數(shù)據(jù)。這聽起來似乎有些風(fēng)險(xiǎn)滔驶,的確也是這樣遇革,本章稍后將介紹這個(gè)問題。不過揭糕,共享變量使線程之間的通信比進(jìn)程之間的通信更有效萝快、更容易。此外著角,在有些操作系統(tǒng)中杠巡,與進(jìn)程相比較,線程更“輕量級(jí)”雇寇,創(chuàng)建氢拥、撤銷一個(gè)線程比啟動(dòng)新進(jìn)程的開銷要小很多。

也就是說锨侯,進(jìn)程的數(shù)據(jù)是隔離的嫩海,而線程是共享的。

????????在實(shí)際應(yīng)用中囚痴,多線程非常有用叁怪。例如,一個(gè)瀏覽器可以同時(shí)下載幾幅圖片深滚。一個(gè) Web 服務(wù)器需要同時(shí)服務(wù)并發(fā)的請(qǐng)求奕谭。圖形用戶界面(GUI)程序用一個(gè)獨(dú)立的線程從宿主操作環(huán)境收集用戶界面事件涣觉。本章將介紹如何為 Java 應(yīng)用程序添加多線程功能。

多線程可以在很多的場(chǎng)景中應(yīng)用

????????溫馨提示:多線程編程可能會(huì)變得相當(dāng)復(fù)雜血柳。本章涵蓋了應(yīng)用程序員可能需要的所有工具官册。盡管如此,對(duì)于更復(fù)雜的系統(tǒng)級(jí)程序設(shè)計(jì)难捌,建議參看更高級(jí)的參考文獻(xiàn)膝宁,例如,Brian Goetz 等撰寫的《Java Concurrency in Pratice》(Addison-Wesly Professional根吁,2006)员淫。

這里學(xué)習(xí)的內(nèi)容比較基礎(chǔ),學(xué)完這里之后可以看一下比較高級(jí)的書击敌,我目前推薦《Java并發(fā)編程的藝術(shù)》介返。

12.1 什么是線程

????????首先來看一個(gè)使用了兩個(gè)線程的簡(jiǎn)單程序。這個(gè)程序可以在銀行賬戶之間完成資金轉(zhuǎn)賬沃斤。我們使用了一個(gè) Bank 類映皆,它可以存儲(chǔ)給定數(shù)目的賬戶的余額。transfer 方法將一定金額從一個(gè)賬戶轉(zhuǎn)移到另一個(gè)賬戶轰枝。具體實(shí)現(xiàn)見程序清單 12-2捅彻。

這里演示了有兩個(gè)線程的例子,非常簡(jiǎn)單的資金轉(zhuǎn)賬的例子鞍陨。

程序清單 12-1

package threads;

/**
 * @author Cay Horstmann
 * @version 1.30 2004-08-01
 */
public class ThreadTest {

    public static final int DELAY = 10;
    public static final int STEPS = 100;
    public static final double MAX_AMOUNT = 1000;

    public static void main(String[] args) {
        Bank bank = new Bank(4, 100000);
        Runnable task1 = () -> {
            try {
                for (int i = 0; i < STEPS; i++) {
                    double amount = MAX_AMOUNT * Math.random();
                    bank.transfer(0, 1, amount);
                    Thread.sleep((long) (DELAY * Math.random()));
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        Runnable task2 = () -> {
            try {
                for (int i = 0; i < STEPS; i++) {
                    double amount = MAX_AMOUNT * Math.random();
                    bank.transfer(2, 3, amount);
                    Thread.sleep((long) (DELAY * Math.random()));
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        new Thread(task1).start();
        new Thread(task2).start();
    }

}

程序清單 12-2

package threads;

import java.util.Arrays;

/**
 * A bank with a number of bank accounts.
 */
public class Bank {

    private final double[] accounts;

    /**
     * Constructs the bank.
     *
     * @param n              the number of accounts
     * @param initialBalance the initial balance for each account
     */
    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        Arrays.fill(accounts, initialBalance);
    }

    /**
     * Transfers money from one account to another.
     *
     * @param from   the account to transfer from
     * @param to     the account to transfer to
     * @param amount the amount to transfer
     */
    public void transfer(int from, int to, double amount) {
        if (accounts[from] < amount) {
            return;
        }
        System.out.print(Thread.currentThread());
        accounts[from] -= amount;
        System.out.printf(" %10.2f from %d to %d", amount, from, to);
        accounts[to] += amount;
        System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
    }

    /**
     * Gets the sum of all account balances.
     *
     * @return the total balance
     */
    public double getTotalBalance() {
        double sum = 0;

        for (double a : accounts) {
            sum += a;
        }
        return sum;
    }

    /**
     * Gets the number of accounts in the bank.
     *
     * @return the number of accounts
     */
    public int size() {
        return accounts.length;
    }

}

這里先把代碼放出來步淹,以便理解,原文是將代碼放的很靠后

????????在第一個(gè)線程中诚撵,我們將錢從賬戶 0 轉(zhuǎn)移到賬戶 1缭裆。第二個(gè)線程將錢從賬戶 2 轉(zhuǎn)移到賬戶 3。

兩個(gè)線程分別實(shí)現(xiàn)了什么目標(biāo)寿烟。

????????下面是在一個(gè)單獨(dú)的線程中運(yùn)行一個(gè)任務(wù)的簡(jiǎn)單過程:

  1. 將執(zhí)行這個(gè)任務(wù)的代碼放在一個(gè)類的 run 方法中澈驼,這個(gè)類要實(shí)現(xiàn) Runnable 接口。Runnable 接口非常簡(jiǎn)單筛武,只有一個(gè)方法:
public interface Runnable {
    public abstract void run();
}

????????由于 Runnable 是一個(gè)函數(shù)式接口缝其,可以用一個(gè) lambda 表達(dá)式創(chuàng)建一個(gè)實(shí)例:

Runnable r = () -> { task code };

我一般是使用 IDEA 的 IDE 編輯器,寫的時(shí)候直接將接口 new 出來徘六,將里面要實(shí)現(xiàn)的接口實(shí)現(xiàn)了内边,這時(shí)候 IDE會(huì)提示你可以省略new 的過程,使用 IDE 自動(dòng)功能可以方便我們實(shí)現(xiàn) lambda待锈,不需要去記憶寫法漠其。

  1. 從這個(gè) Runnable 構(gòu)造一個(gè) Thread 對(duì)象:
Thread t = new Thread(r);
  1. 啟動(dòng)線程:
t.start();

????????為了建立單獨(dú)的線程來完成轉(zhuǎn)賬,我們只需要把轉(zhuǎn)賬代碼放在一個(gè) Runnable 的 run 方法中,然后啟動(dòng)一個(gè)線程:

Runnable r = () -> {
    try {
        for (int i = 0; i < 100; i++) {
            double amount = MAX_AMOUNT * Math.random();
            bank.transfer(0, 1, amount);
            Thread.sleep((long) (DELAY * Math.random()));
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};
Thread t = new Thread(r);
t.start();

上面的代碼只是展示了部分代碼和屎。

????????對(duì)于給定的步驟數(shù)拴驮,這個(gè)線程會(huì)轉(zhuǎn)賬一個(gè)隨機(jī)金額,然后休眠一個(gè)隨機(jī)的延遲時(shí)間柴信。

????????我們要捕獲 sleep 方法有可能拋出的 InterruptException 異常套啤。這個(gè)異常會(huì)在 12.3.1 節(jié)討論。一般來說颠印,中斷用來請(qǐng)求終止一個(gè)線程纲岭。相應(yīng)地抹竹,出現(xiàn) InterruptedException 時(shí)线罕,run 方法會(huì)退出。

????????程序還會(huì)啟動(dòng)第二個(gè)線程窃判,它從賬戶 2 向賬戶 3 轉(zhuǎn)賬钞楼。運(yùn)行這個(gè)程序時(shí),可以得到類似這樣的輸出:

我們這里可以運(yùn)行示例代碼12-1和12-2的代碼

Thread[Thread-1,5,main]     907.97 from 2 to 3 Total Balance:  400000.00
Thread[Thread-0,5,main]     883.06 from 0 to 1 Total Balance:  400000.00
Thread[Thread-1,5,main]     188.52 from 2 to 3 Total Balance:  400000.00
Thread[Thread-0,5,main]     469.69 from 0 to 1 Total Balance:  400000.00
Thread[Thread-0,5,main]     547.20 from 0 to 1 Total Balance:  400000.00
Thread[Thread-0,5,main]     942.84 from 0 to 1 Total Balance:  400000.00
Thread[Thread-0,5,main]     183.58 from 0 to 1 Total Balance:  400000.00
Thread[Thread-1,5,main]     501.09 from 2 to 3 Total Balance:  400000.00
Thread[Thread-1,5,main]      65.10 from 2 to 3 Total Balance:  400000.00
Thread[Thread-0,5,main]     324.28 from 0 to 1 Total Balance:  400000.00
Thread[Thread-1,5,main]     127.08 from 2 to 3 Total Balance:  400000.00
......

????????可以看到袄琳,兩個(gè)線程的輸出是交錯(cuò)的询件,這說明它們?cè)诓l(fā)運(yùn)行。實(shí)際上唆樊,兩個(gè)輸出行交錯(cuò)顯示時(shí)宛琅,輸出有時(shí)會(huì)有些混亂。

這里只是告訴我們逗旁,證明了這兩個(gè)線程是交錯(cuò)運(yùn)行的嘿辟。

????????你要了解的就是這些!現(xiàn)在你已經(jīng)知道了如何并發(fā)地運(yùn)行任務(wù)片效。這一章余下的部分會(huì)介紹如何控制線程之間的交互红伦。

這里告訴我們這里只能證明線程是并行執(zhí)行的,其他要等到后面才能告訴我們淀衣。

????????程序的完整代碼見程序清單 12-1昙读。

程序清單 12-1 已經(jīng)在上面了。

注釋:還可以通過建立 Thread 類的一個(gè)子類來定義線程膨桥,如下所示:

class MyThread extends Thread

{

public void run()

{

  task code

}

}

然后可以構(gòu)建這個(gè)子類的一個(gè)對(duì)象蛮浑,并調(diào)用它的 start 方法箱靴。不過唬复,現(xiàn)在不再推薦這種方法。應(yīng)當(dāng)把要并行運(yùn)行的任務(wù)與運(yùn)行機(jī)制解耦合呐芥。如果有多個(gè)任務(wù)介牙,為每個(gè)任務(wù)分別創(chuàng)建一個(gè)單獨(dú)的線程開銷會(huì)太大壮虫。實(shí)際上,可以使用一個(gè)線程池,參見 12.6.2 節(jié)的介紹囚似。

警告:不要調(diào)用 Thread 類或 Runnable 對(duì)象的 run 方法剩拢。直接調(diào)用 run 方法只會(huì)在同一個(gè)線程中執(zhí)行這個(gè)任務(wù)——而沒有啟動(dòng)新的線程。實(shí)際上饶唤,應(yīng)當(dāng)調(diào)用 Thread.start 方法徐伐,這會(huì)創(chuàng)建一個(gè)執(zhí)行 run 方法的新線程。

12.2 線程狀態(tài)

線程可以有如下 6 種狀態(tài):

12.3 線程屬性

????????下面幾節(jié)將討論線程的各種屬性募狂,包括中斷的狀態(tài)办素、守護(hù)線程、未捕獲異常的處理器以及不應(yīng)使用的一些遺留特性祸穷。

12.3.1 中斷線程

????????當(dāng)線程的 run 方法執(zhí)行方法體重最后一條語(yǔ)句后再執(zhí)行 return 語(yǔ)句返回時(shí)性穿,或者出現(xiàn)了方法中沒有捕獲的異常時(shí),線程將終止雷滚。在 Java 的早期版本中需曾,還有一個(gè) stop 方法,其他線程可以調(diào)用這個(gè)方法來終止一個(gè)線程祈远。但是這個(gè)方法現(xiàn)在已經(jīng)被廢棄了呆万。12.4.13 節(jié)將討論它被廢棄的緣由。

這里解釋了如果我們要讓正在執(zhí)行的方法中斷车份,需要什么辦法谋减,我的理解,中斷即結(jié)束扫沼,也就是說不能再恢復(fù)了出爹,這里給我們提供了兩種辦法,第一種就是run 方法體中的代碼完全執(zhí)行完了充甚,這很好理解以政,第二個(gè)就是在run 方法的執(zhí)行中出現(xiàn)了未捕獲的異常,會(huì)導(dǎo)致線程執(zhí)行的中斷伴找。這里暫時(shí)不演示了盈蛮,后面有詳細(xì)的講解。

????????除了已經(jīng)廢棄的 stop 方法技矮,沒有辦法可以強(qiáng)制線程終止抖誉。不過,interrupt 方法可以用來請(qǐng)求終止一個(gè)線程衰倦。

我們沒有辦法強(qiáng)制一個(gè)線程停止運(yùn)行了袒炉,通過外力的方式不行了,但是我們可以請(qǐng)求它終止樊零,當(dāng)然請(qǐng)求了也不一定終止我磁,所以這得看情況了孽文,后面會(huì)介紹具體情況。

????????當(dāng)對(duì)一個(gè)線程調(diào)用 interrupt 方法時(shí)夺艰,就會(huì)設(shè)置線程的中斷狀態(tài)芋哭。這是每個(gè)線程都有的 boolean 標(biāo)志。每個(gè)線程都應(yīng)該不時(shí)地檢查這個(gè)標(biāo)志郁副,以判斷線程是否被中斷减牺。

這個(gè)標(biāo)志是我們自己給當(dāng)前線程或者是我們自己從外面給某個(gè)線程設(shè)置的標(biāo)志,我們的線程在運(yùn)行的時(shí)候存谎,應(yīng)該經(jīng)常主動(dòng)的通過代碼來驗(yàn)證一下這個(gè)標(biāo)志拔疚,以便達(dá)到讓我們的程序自己停止的目的。

????????要想得出是否設(shè)置了中斷狀態(tài)既荚,首先調(diào)用靜態(tài)的 Thread.currentThread 方法獲得當(dāng)前線程稚失,然后調(diào)用 isInterrupted 方法:

我們可以通過 Thread.currentThread 獲取線程,再?gòu)倪@個(gè)線程中獲取 isInterrupted 方法來判斷當(dāng)前線程是否被設(shè)置了終止標(biāo)志固以。

while (!Thread.currentThread().isInterrupted() && more work to od) {
    do more work
}

????????但是墩虹,如果線程被阻塞嘱巾,就無(wú)法檢查中斷狀態(tài)憨琳。這里就要引入 InterruptedException 異常。當(dāng)在一個(gè)被 sleepwait 調(diào)用阻塞的線程上調(diào)用 interrupt 方法時(shí)旬昭,那個(gè)阻塞調(diào)用(即 sleepwait 調(diào)用)將被一個(gè) InterruptedException 異常中斷篙螟。(有一些阻塞 I/O 調(diào)用不能被中斷,對(duì)此應(yīng)該考慮選擇可中斷的調(diào)用问拘。有關(guān)細(xì)節(jié)請(qǐng)參看卷 2 的第 2 章和第 4 章遍略。)

比如我們的代碼像上面一樣,只有 while 方法體中的代碼執(zhí)行完成后骤坐,才能檢查中斷標(biāo)志绪杏,那代碼很有可能阻塞在 while 方法體中,而一直無(wú)法走到 while 中的判斷語(yǔ)句中纽绍。如果我們的線程正在被 sleep 或 wait 阻塞蕾久,我們調(diào)用 interrupt 方法的時(shí)候,就會(huì)拋出 InterruptedException 異常了拌夏,其實(shí)我們可以看一下代碼僧著,在方法聲明的時(shí)候其實(shí)都聲明拋出 InterruptedException 異常的,所以這里可以總結(jié)為障簿,如果我們的線程被一些聲明了 InterruptedException 的方法阻塞了盹愚,那么調(diào)用 interrupt 方法,該線程就會(huì)直接拋出 InterruptedException 異常了站故。所以皆怕,我們這節(jié)在開頭的時(shí)候說了,如果線程在遇到未捕獲的異常的時(shí)候,會(huì)終止運(yùn)行愈腾,所以這個(gè)拋出 InterruptedException 的線程朗兵,如果我們沒有在捕獲這個(gè)異常,好好的處理異常的話顶滩,那么這個(gè)異常就會(huì)終止這個(gè)線程的運(yùn)行了余掖。

????????沒有任何語(yǔ)言要求被中斷的線程應(yīng)當(dāng)終止。中斷一個(gè)線程只是要引起它的注意礁鲁。被中斷的線程可以決定如何響應(yīng)中斷盐欺。某些線程非常重要,所以應(yīng)該處理這個(gè)異常仅醇,然后再繼續(xù)執(zhí)行冗美。但是,更普遍的情況是析二,線程只希望將中斷解釋為一個(gè)終止請(qǐng)求粉洼。這種線程的 run 方法具有如下形式:

Runnable r = () -> {
    try {
        ...
        while (!Thread.currentThread().isInterrupted() && more work to do) {
            do more work
        }
    } catch (InterruptedException e) {
        // thread was interrupted during sleep or wait
    } finally {
        cleanup,if required
    }
    // exiting the run method terminates the thread
}

這意思也就是說,即使我們調(diào)用了 interrupt 函數(shù)叶摄,線程也不是必須要停下來的属韧。這只是為了告訴線程一個(gè)信號(hào)而已。就像上面的代碼展示的那樣蛤吓,即使看到了標(biāo)志是中斷的宵喂,我們也可以不中斷,當(dāng)然也可以直接在代碼中判斷為終止会傲,如果是比較重要的線程锅棕,就必須考慮在調(diào)用了 interrupt 函數(shù)以后會(huì)不會(huì)拋出 InterruptedException 異常了,所以這個(gè)時(shí)候我們應(yīng)該是 catch 住這個(gè)異常淌山,處理好后退出裸燎。

????????如果在每次工作迭代之后都調(diào)用 sleep 方法(或者其他可中斷方法),isInterrupted 檢查既沒有必要也沒有用處泼疑。如果設(shè)置了中斷狀態(tài)德绿,此時(shí)倘若調(diào)用 sleep 方法,它不會(huì)休眠王浴。實(shí)際上脆炎,它會(huì)清除中斷狀態(tài)(!)并拋出 InterruptedException氓辣。因此秒裕,如果你的循環(huán)調(diào)用了 sleep,不要檢測(cè)中斷狀態(tài)钞啸,而應(yīng)當(dāng)捕獲 InterruptedException 異常几蜻,如下所示:

這里的意思是喇潘,如果我們的線程代碼中有類似會(huì)拋出 InterruptedException 的代碼,就沒必要再去判斷什么 isInterrupted 的狀態(tài)了梭稚,因?yàn)楹芸赡苁菦]有用的颖低,還不如去捕獲 InterruptedException 異常。

Runnable r = () -> {
    try {
        ...
        while (more work to do) {
            do more work
            Thread.sleep(delay);
        }
    } catch (InterruptedException e) {
        // thread was interrupted during sleep
    } finally {
        cleanup, if required
    }
    // exiting the run method terminates the thread
};

注釋:有兩個(gè)非常類似的方法弧烤,interruptedisInterrupted忱屑。interrupted 方法是一個(gè)靜態(tài)方法,它檢查當(dāng)前線程是否被中斷暇昂。而且莺戒,調(diào)用 interrupted 方法會(huì)清除該線程的中斷狀態(tài)。另一方面急波,isInterrupted 方法是一個(gè)實(shí)例方法从铲,可以用來檢查是否有線程被中斷。調(diào)用這個(gè)方法不會(huì)改變中斷狀態(tài)澄暮。

12.4 同步

????????在大多數(shù)實(shí)際的多線程應(yīng)用中名段,兩個(gè)或兩個(gè)以上的線程需要共享對(duì)同一數(shù)據(jù)的存取。如果兩個(gè)線程存取同一個(gè)對(duì)象泣懊,并且每個(gè)線程分別調(diào)用了一個(gè)修改該對(duì)象狀態(tài)的方法伸辟,會(huì)發(fā)生什么呢?可以想見嗅定,這兩個(gè)線程會(huì)相互覆蓋自娩。取決于線程訪問數(shù)據(jù)的次序用踩,可能會(huì)導(dǎo)致對(duì)象被破壞渠退。這種情況稱為競(jìng)態(tài)條件(race condition)。

兩個(gè)線程競(jìng)爭(zhēng)同一塊內(nèi)存脐彩,操作同一塊內(nèi)存的數(shù)據(jù)碎乃,會(huì)產(chǎn)生競(jìng)爭(zhēng)的關(guān)系。

12.4.1 競(jìng)態(tài)條件的一個(gè)例子

????????為了避免多線程破壞共享數(shù)據(jù)惠奸,必須學(xué)習(xí)如何同步存取梅誓。在本節(jié)中,你會(huì)看到如果沒有使用同步會(huì)發(fā)生什么佛南。在下一節(jié)中梗掰,你將會(huì)看到如何同步數(shù)據(jù)存取。

多線程有時(shí)候會(huì)破壞共享數(shù)據(jù)嗅回,如果沒有很好的處理及穗,會(huì)產(chǎn)生問題。

????????在下面的測(cè)試程序中绵载,還是考慮我們模擬的銀行埂陆。與 12.1 節(jié)中的例子不同苛白,我們要隨機(jī)地選擇從哪個(gè)源賬戶轉(zhuǎn)賬到哪個(gè)目標(biāo)賬戶。由于這會(huì)產(chǎn)生問題焚虱,所以下面再來仔細(xì)查看 Bank 類 transfer 方法的代碼购裙。

    public void transfer(int from, int to, double amount) {
        // CAUTIO: unsafe when called from multiple threads
        System.out.print(Thread.currentThread());
        accounts[from] -= amount;
        System.out.printf(" %10.2f from %d to %d", amount, from, to);
        accounts[to] += amount;
        System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
    }

程序清單 12-3

package unsynch;

import threads.Bank;

/**
 * This program shows data corruption when multiple threads access a data structure.
 *
 * @author Cay Horstmann
 * @version 1.32 2018-04-10
 */
public class UnsynchBankTest {
    public static final int NACCOUNTS = 100;
    public static final double INITIAL_BALANCE = 1000;
    public static final double MAX_AMOUNT = 1000;
    public static final int DELAY = 10;

    public static void main(String[] args) {
        Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
        for (int i = 0; i < NACCOUNTS; i++) {
            int fromAccount = i;
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    try {
                        while (true) {
                            int toAccount = (int) (bank.size() * Math.random());
                            double amount = MAX_AMOUNT * Math.random();
                            bank.transfer(fromAccount, toAccount, amount);
                            Thread.sleep((long) (DELAY * Math.random()));
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
            Thread t = new Thread(r);
            t.start();
        }
    }
}

這里面 Bank 在初始化的時(shí)候,會(huì)為每個(gè)賬戶都分配 1000 塊錢鹃栽,然后循環(huán)運(yùn)行 100 個(gè)線程躏率,因?yàn)檠h(huán)的個(gè)數(shù)和賬戶的個(gè)數(shù)是相等的,每個(gè)線程都基本是隨機(jī)的從一個(gè)賬戶向另外的賬戶轉(zhuǎn)賬民鼓,運(yùn)行可以看到結(jié)果賬戶總額不是總是正確的禾锤。

12.4.2 競(jìng)態(tài)條件詳解

????????上一節(jié)中運(yùn)行了一個(gè)程序,其中有幾個(gè)線程會(huì)更新銀行賬戶余額摹察。一段時(shí)間之后恩掷,不知不覺地出現(xiàn)了錯(cuò)誤,可能有些錢會(huì)丟失供嚎,也可能幾個(gè)賬戶同時(shí)有錢進(jìn)賬黄娘。當(dāng)兩個(gè)線程試圖同時(shí)更新同一個(gè)賬戶時(shí),就會(huì)出現(xiàn)這個(gè)問題克滴。假設(shè)兩個(gè)線程同時(shí)執(zhí)行指令

accounts[to] += amount;

上面運(yùn)行的結(jié)果就可以看出來逼争,同時(shí)運(yùn)行的時(shí)候,賬戶余額是不對(duì)的劝赔。

注釋:實(shí)際上可以查看執(zhí)行這個(gè)類中每一個(gè)語(yǔ)句的虛擬機(jī)字節(jié)碼誓焦。運(yùn)行以下命令

javap -c -v Bank

實(shí)際上執(zhí)行的命令是

javap -c -v Bank.class

這里我建議加上.class

對(duì) Bank.class 文件進(jìn)行反編譯。例如着帽,代碼行

accounts[to] += amount;

會(huì)轉(zhuǎn)換為下面的字節(jié)碼:

D:\IdeaProjects\untitled1\target\classes\threads>javap -c -v Bank
警告: 文件 .\Bank.class 不包含類 Bank
Classfile /D:/IdeaProjects/untitled1/target/classes/threads/Bank.class
  Last modified 2021年3月19日; size 1442 bytes
  SHA-256 checksum 81004a4d4033d9f6db32543f79b5fbc2ba904eb575f37a756d8aaf3c59385ee7
  Compiled from "Bank.java"
public class threads.Bank
  minor version: 0
  major version: 59
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // threads/Bank
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 4, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // threads/Bank.accounts:[D
   #8 = Class              #10            // threads/Bank
   #9 = NameAndType        #11:#12        // accounts:[D
  #10 = Utf8               threads/Bank
  #11 = Utf8               accounts
  #12 = Utf8               [D
  #13 = Methodref          #14.#15        // java/util/Arrays.fill:([DD)V
  #14 = Class              #16            // java/util/Arrays
  #15 = NameAndType        #17:#18        // fill:([DD)V
  #16 = Utf8               java/util/Arrays
  #17 = Utf8               fill
  #18 = Utf8               ([DD)V
  #19 = Fieldref           #20.#21        // java/lang/System.out:Ljava/io/PrintStream;
  #20 = Class              #22            // java/lang/System
  #21 = NameAndType        #23:#24        // out:Ljava/io/PrintStream;
  #22 = Utf8               java/lang/System
  #23 = Utf8               out
  #24 = Utf8               Ljava/io/PrintStream;
  #25 = Methodref          #26.#27        // java/lang/Thread.currentThread:()Ljava/lang/Thread;
  #26 = Class              #28            // java/lang/Thread
  #27 = NameAndType        #29:#30        // currentThread:()Ljava/lang/Thread;
  #28 = Utf8               java/lang/Thread
  #29 = Utf8               currentThread
  #30 = Utf8               ()Ljava/lang/Thread;
  #31 = Methodref          #32.#33        // java/io/PrintStream.print:(Ljava/lang/Object;)V
  #32 = Class              #34            // java/io/PrintStream
  #33 = NameAndType        #35:#36        // print:(Ljava/lang/Object;)V
  #34 = Utf8               java/io/PrintStream
  #35 = Utf8               print
  #36 = Utf8               (Ljava/lang/Object;)V
  #37 = String             #38            //  %10.2f from %d to %d
  #38 = Utf8                %10.2f from %d to %d
  #39 = Methodref          #40.#41        // java/lang/Double.valueOf:(D)Ljava/lang/Double;
  #40 = Class              #42            // java/lang/Double
  #41 = NameAndType        #43:#44        // valueOf:(D)Ljava/lang/Double;
  #42 = Utf8               java/lang/Double
  #43 = Utf8               valueOf
  #44 = Utf8               (D)Ljava/lang/Double;
  #45 = Methodref          #46.#47        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
  #46 = Class              #48            // java/lang/Integer
  #47 = NameAndType        #43:#49        // valueOf:(I)Ljava/lang/Integer;
  #48 = Utf8               java/lang/Integer
  #49 = Utf8               (I)Ljava/lang/Integer;
  #50 = Methodref          #32.#51        // java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
  #51 = NameAndType        #52:#53        // printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
  #52 = Utf8               printf
  #53 = Utf8               (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
  #54 = String             #55            //  Total Balance: %10.2f%n
  #55 = Utf8                Total Balance: %10.2f%n
  #56 = Methodref          #8.#57         // threads/Bank.getTotalBalance:()D
  #57 = NameAndType        #58:#59        // getTotalBalance:()D
  #58 = Utf8               getTotalBalance
  #59 = Utf8               ()D
  #60 = Utf8               (ID)V
  #61 = Utf8               Code
  #62 = Utf8               LineNumberTable
  #63 = Utf8               LocalVariableTable
  #64 = Utf8               this
  #65 = Utf8               Lthreads/Bank;
  #66 = Utf8               n
  #67 = Utf8               I
  #68 = Utf8               initialBalance
  #69 = Utf8               D
  #70 = Utf8               transfer
  #71 = Utf8               (IID)V
  #72 = Utf8               from
  #73 = Utf8               to
  #74 = Utf8               amount
  #75 = Utf8               StackMapTable
  #76 = Utf8               a
  #77 = Utf8               sum
  #78 = Class              #12            // "[D"
  #79 = Utf8               size
  #80 = Utf8               ()I
  #81 = Utf8               SourceFile
  #82 = Utf8               Bank.java
{
  public threads.Bank(int, double);
    descriptor: (ID)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=3
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iload_1
         6: newarray       double
         8: putfield      #7                  // Field accounts:[D
        11: aload_0
        12: getfield      #7                  // Field accounts:[D
        15: dload_2
        16: invokestatic  #13                 // Method java/util/Arrays.fill:([DD)V
        19: return
      LineNumberTable:
        line 18: 0
        line 19: 4
        line 20: 11
        line 21: 19
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      20     0  this   Lthreads/Bank;
            0      20     1     n   I
            0      20     2 initialBalance   D

  public void transfer(int, int, double);
    descriptor: (IID)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=7, locals=5, args_size=4
         0: aload_0
         1: getfield      #7                  // Field accounts:[D
         4: iload_1
         5: daload
         6: dload_3
         7: dcmpg
         8: ifge          12
        11: return
        12: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
        15: invokestatic  #25                 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
        18: invokevirtual #31                 // Method java/io/PrintStream.print:(Ljava/lang/Object;)V
        21: aload_0
        22: getfield      #7                  // Field accounts:[D
        25: iload_1
        26: dup2
        27: daload
        28: dload_3
        29: dsub
        30: dastore
        31: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
        34: ldc           #37                 // String  %10.2f from %d to %d
        36: iconst_3
        37: anewarray     #2                  // class java/lang/Object
        40: dup
        41: iconst_0
        42: dload_3
        43: invokestatic  #39                 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
        46: aastore
        47: dup
        48: iconst_1
        49: iload_1
        50: invokestatic  #45                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        53: aastore
        54: dup
        55: iconst_2
        56: iload_2
        57: invokestatic  #45                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        60: aastore
        61: invokevirtual #50                 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
        64: pop
        65: aload_0
        66: getfield      #7                  // Field accounts:[D
        69: iload_2
        70: dup2
        71: daload
        72: dload_3
        73: dadd
        74: dastore
        75: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
        78: ldc           #54                 // String  Total Balance: %10.2f%n
        80: iconst_1
        81: anewarray     #2                  // class java/lang/Object
        84: dup
        85: iconst_0
        86: aload_0
        87: invokevirtual #56                 // Method getTotalBalance:()D
        90: invokestatic  #39                 // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
        93: aastore
        94: invokevirtual #50                 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
        97: pop
        98: return
      LineNumberTable:
        line 31: 0
        line 32: 11
        line 34: 12
        line 35: 21
        line 36: 31
        line 37: 65
        line 38: 75
        line 39: 98
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      99     0  this   Lthreads/Bank;
            0      99     1  from   I
            0      99     2    to   I
            0      99     3 amount   D
      StackMapTable: number_of_entries = 1
        frame_type = 12 /* same */

  public double getTotalBalance();
    descriptor: ()D
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=4, locals=8, args_size=1
         0: dconst_0
         1: dstore_1
         2: aload_0
         3: getfield      #7                  // Field accounts:[D
         6: astore_3
         7: aload_3
         8: arraylength
         9: istore        4
        11: iconst_0
        12: istore        5
        14: iload         5
        16: iload         4
        18: if_icmpge     38
        21: aload_3
        22: iload         5
        24: daload
        25: dstore        6
        27: dload_1
        28: dload         6
        30: dadd
        31: dstore_1
        32: iinc          5, 1
        35: goto          14
        38: dload_1
        39: dreturn
      LineNumberTable:
        line 47: 0
        line 49: 2
        line 50: 27
        line 49: 32
        line 52: 38
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           27       5     6     a   D
            0      40     0  this   Lthreads/Bank;
            2      38     1   sum   D
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 14
          locals = [ class threads/Bank, double, class "[D", int, int ]
          stack = []
        frame_type = 248 /* chop */
          offset_delta = 23

  public int size();
    descriptor: ()I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #7                  // Field accounts:[D
         4: arraylength
         5: ireturn
      LineNumberTable:
        line 61: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lthreads/Bank;
}
SourceFile: "Bank.java"

我們可以自行找到 transfer 方法那一段的字節(jié)碼杂伟,我們可以看到對(duì)數(shù)據(jù)的操作是分為很多行分別執(zhí)行的。

這些代碼的含義無(wú)關(guān)緊要仍翰。重要的是這個(gè)增加命令是由多條指令組成的赫粥,執(zhí)行這些指令的線程可以在任何一條指令上被中斷。

????????出現(xiàn)這種破壞的可能性有多大呢予借?在一個(gè)有多個(gè)內(nèi)核的現(xiàn)代處理器上越平,出問題的風(fēng)險(xiǎn)相當(dāng)高。我們將打印語(yǔ)句和更新余額的語(yǔ)句交錯(cuò)執(zhí)行灵迫,以提高觀察到這種問題的概率秦叛。

多個(gè)內(nèi)核的CPU會(huì)放大這種效果,因?yàn)橥聲?huì)有更多的線程執(zhí)行打印輸出可能會(huì)讓執(zhí)行線程陷入等待的幾率增加瀑粥,方便我們觀察挣跋。

????????如果刪除打印語(yǔ)句,出問題的風(fēng)險(xiǎn)會(huì)降低利凑,因?yàn)槊總€(gè)線程在再次休眠之前所做的工作很少浆劲,調(diào)度器不太可能在線程的計(jì)算過程中搶占它的運(yùn)行權(quán)嫌术。但是,產(chǎn)生破壞的風(fēng)險(xiǎn)并沒有完全消失牌借。如果在負(fù)載很重的機(jī)器上運(yùn)行大量線程度气,那么,即使刪除了打印語(yǔ)句膨报,程序依然會(huì)出錯(cuò)磷籍。這種錯(cuò)誤可能幾分鐘、幾小時(shí)或幾天后才出現(xiàn)现柠。坦白地說院领,對(duì)程序員而言,最糟糕的事情莫過于這種不定期地出現(xiàn)錯(cuò)誤够吩。

這里我們可以認(rèn)為的讓風(fēng)險(xiǎn)出現(xiàn)的機(jī)會(huì)大大降低比然,但是不能根本性的解決問題,在并發(fā)度非常高的場(chǎng)景周循,還是會(huì)時(shí)不時(shí)的出現(xiàn)錯(cuò)誤的狀況强法。

????????真正的問題是 transfer 方法可能會(huì)在執(zhí)行到中間被中斷。如果能夠確保線程失去控制之前方法已經(jīng)運(yùn)行完成湾笛,那么銀行賬戶對(duì)象的狀態(tài)就不會(huì)被破壞饮怯。

我們從代碼上看,transfer 是不太可能從中間中斷的嚎研,首先 stop 方法已經(jīng)棄用了蓖墅,我們可以調(diào)用 interrupt 方法,但是我們即使設(shè)置了這個(gè)中斷線程也還可以不中斷临扮,繼續(xù)執(zhí)行论矾,或者正好運(yùn)行到了 sleep 處,那么會(huì)拋出異常停止該線程公条,但是也不會(huì)在 transfer 中間中斷拇囊,我的理解是那種比較暴力的因素,比如說靶橱,oom了,整個(gè) java 進(jìn)程崩潰了路捧,這種情況是有可能出現(xiàn)的关霸,所以在這種情況下如何保證 transfer 不會(huì)從中間中斷就是一個(gè)問題了。

12.4.3 鎖對(duì)象

????????有兩種機(jī)制可防止并發(fā)訪問代碼塊杰扫。Java 語(yǔ)言提供了一個(gè) synchronized 關(guān)鍵字來達(dá)到這一目的队寇,另外 Java 5 引入了 ReentrantLock 類。synchronized 關(guān)鍵字會(huì)自動(dòng)提供一個(gè)鎖以及相關(guān)的“條件”章姓,對(duì)于大多數(shù)需要顯式鎖的情況佳遣,這種機(jī)制功能很強(qiáng)大识埋,也很便利。不過零渐,我們相信在分別了解鎖和條件的內(nèi)容之后窒舟,就能更容易地理解 synchronized 關(guān)鍵字。java.util.concurrent 框架為這些基礎(chǔ)機(jī)制提供了單獨(dú)的類诵盼,有關(guān)內(nèi)容會(huì)在本節(jié)以及 12.4.4 節(jié)解釋惠豺。一旦理解了這些基礎(chǔ),我們會(huì)在 12.4.5 節(jié)介紹 synchronized 關(guān)鍵字风宁。

目前有兩種方式可以防止并發(fā)訪問錯(cuò)誤的產(chǎn)生洁墙,一種是加 synchronized 關(guān)鍵字,一種是 ReentrantLock 類戒财。

????????用 ReetrantLock 保護(hù)代碼塊的基本結(jié)構(gòu)如下:

// a ReentrantLock object
myLock.lock();
try {
    critical section
} finally {
    // make sure the lock is unlocked even if an exception is thrown
    myLock.unlock();
}

????????這個(gè)結(jié)構(gòu)確保任何時(shí)刻只有一個(gè)線程進(jìn)入臨界區(qū)热监。一旦一個(gè)線程鎖定了對(duì)象,其他任何線程都無(wú)法通過 lock 語(yǔ)句饮寞。當(dāng)其他線程調(diào)用 lock 時(shí)狼纬,它們會(huì)暫停,直到第一個(gè)線程釋放這個(gè)鎖對(duì)象骂际。

myLock.lock();就是上鎖的意思疗琉,在這里就開始鎖住了資源的訪問了,如果這個(gè)線程沒有釋放鎖歉铝,其他線程是無(wú)法訪問這些資源的盈简。

警告:要把 unlock 操作包括在 finally 字句中,這一點(diǎn)至關(guān)重要太示。如果在臨界區(qū)的代碼拋出一個(gè)異常柠贤,鎖必須釋放。否則类缤,其他線程將永遠(yuǎn)阻塞臼勉。

注釋:使用鎖時(shí),就不能使用 try-with-resources 語(yǔ)句餐弱。首先宴霸,解鎖方法名不是 close。不過膏蚓,即使將它重命名瓢谢,try-with-resources 語(yǔ)句也無(wú)法正常工作。它的首部希望聲明一個(gè)新變量驮瞧。但是如果使用一個(gè)鎖氓扛,你可能想使用多個(gè)線程共享的那個(gè)變量(而不是新變量)。

首先是我們無(wú)法使用 JDK 1.7 引入的 try-with-resources 新特性论笔,其次采郎,我們?cè)谑褂玫臅r(shí)候千所,要保證線程使用的是同一把鎖,如果不是一把鎖蒜埋,鎖就失去了意義淫痰。

????????下面使用一個(gè)鎖來保護(hù) Bank 類的 transfer 方法。

package threads;

import java.util.Arrays;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * A bank with a number of bank accounts.
 */
public class Bank {

    private final double[] accounts;

    private Lock bankLock = new ReentrantLock();

    /**
     * Constructs the bank.
     *
     * @param n              the number of accounts
     * @param initialBalance the initial balance for each account
     */
    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        Arrays.fill(accounts, initialBalance);
    }

    /**
     * Transfers money from one account to another.
     *
     * @param from   the account to transfer from
     * @param to     the account to transfer to
     * @param amount the amount to transfer
     */
    public void transfer(int from, int to, double amount) {
        bankLock.lock();
        try {
            if (accounts[from] < amount) {
                return;
            }
            System.out.print(Thread.currentThread());
            accounts[from] -= amount;
            System.out.printf(" %10.2f from %d to %d", amount, from, to);
            accounts[to] += amount;
            System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
        } finally {
            bankLock.unlock();
        }
    }

    /**
     * Gets the sum of all account balances.
     *
     * @return the total balance
     */
    public double getTotalBalance() {
        double sum = 0;

        for (double a : accounts) {
            sum += a;
        }
        return sum;
    }

    /**
     * Gets the number of accounts in the bank.
     *
     * @return the number of accounts
     */
    public int size() {
        return accounts.length;
    }

}

????????假設(shè)一個(gè)線程調(diào)用了 transfer理茎,但是在執(zhí)行結(jié)束前被搶占黑界。再假設(shè)第二個(gè)線程也調(diào)用了 transfer,由于第二個(gè)線程不能獲得鎖皂林,將在調(diào)用 lock 方法時(shí)被阻塞朗鸠。它會(huì)暫停,必須等待第一個(gè)線程執(zhí)行完 transfer 方法础倍。當(dāng)?shù)谝粋€(gè)線程釋放鎖時(shí)烛占,第二個(gè)線程才能開始運(yùn)行(見圖 12-3)。

也就是說沟启,第一個(gè)線程如果獲取了鎖忆家,第二個(gè)線程就無(wú)法再執(zhí)行鎖代碼塊內(nèi)的程序了,當(dāng)然德迹,這是對(duì)于同一個(gè)對(duì)象而言的芽卿。

????????通常我們可能希望保護(hù)會(huì)更新或檢查共享對(duì)象的代碼塊,從而能確信當(dāng)前操作執(zhí)行完之后其他線程才能使用同一個(gè)對(duì)象胳搞。

如果多個(gè)對(duì)象訪問修改同一塊數(shù)據(jù)卸例,那么我們就需要把它鎖起來。

12.4.4 條件對(duì)象

????????通常肌毅,線程進(jìn)入臨界區(qū)后卻發(fā)現(xiàn)只有滿足了某個(gè)條件之后它才能執(zhí)行筷转。可以使用一個(gè)條件對(duì)象來管理那些已經(jīng)獲得了一個(gè)鎖卻不能做有用工作的線程悬而。在這一節(jié)里呜舒,我們會(huì)介紹 Java 庫(kù)中條件對(duì)象的實(shí)現(xiàn)(由于歷史原因,條件對(duì)象經(jīng)常被稱為條件變量(conditional variable))笨奠。

這里我也無(wú)法正確的理解什么是使用一個(gè)條件來管理那些已經(jīng)獲得了一個(gè)鎖卻不能做有用工作的線程是什么含義袭蝗,可能還要往后看才可以。

????????現(xiàn)在來優(yōu)化銀行的模擬程序艰躺。如果一個(gè)賬戶沒有足夠的資金轉(zhuǎn)賬呻袭,我們不希望從這樣的賬戶轉(zhuǎn)出資金。注意不能使用類似下面的代碼:

if (bank.getBalance(from) >= amount) {
    bank.transfer(from, to, amount);
}

之前有個(gè)案例就是轉(zhuǎn)賬腺兴,現(xiàn)在要轉(zhuǎn)賬之前,我們都應(yīng)該要判斷一下賬戶里面是不是還有足夠的資金廉侧,如果沒有資金我們就不應(yīng)該從這樣的賬戶中轉(zhuǎn)出資金了页响。

public double getBalance(int account) {
      return accounts[account];
}

getBalance 方法的代碼應(yīng)該是與上面的代碼類似篓足,直接獲取數(shù)組中索引位置的浮點(diǎn)數(shù)值,并返回闰蚕。這里跟我們說不能使用這段代碼來判斷栈拖,目前我所理解的是,這段代碼判斷和轉(zhuǎn)出操作不是原子的没陡,會(huì)帶來線程安全問題涩哟。

????????在成功地通過這個(gè)測(cè)試之后,但在調(diào)用 transfer 方法之前盼玄,當(dāng)前線程完全有可能被中斷贴彼。

看來這里與我之前推測(cè)的原因一致

if (bank.getBalance(from) >= amount) {
    // thread might be deactivated at this point
    bank.transfer(from, to, amount);
}

上面的代碼注釋告訴我們,在判斷完金額以后埃儿,線程可能會(huì)被中斷器仗。

????????在線程再次運(yùn)行前,賬戶余額可能已經(jīng)低于提款金額童番。必須確保在檢查余額與轉(zhuǎn)賬活動(dòng)之間沒有其他線程修改余額精钮。為此,可以使用一個(gè)鎖來保護(hù)這個(gè)測(cè)試和轉(zhuǎn)賬操作:

我們必須保證這個(gè)操作是原子的剃斧,在互聯(lián)網(wǎng)中我們一般使用分布式鎖來實(shí)現(xiàn)轨香,因?yàn)橐话惴?wù)器上都是集群的,光鎖一個(gè)進(jìn)程用處不打幼东,別的進(jìn)程還能執(zhí)行臂容。所以這個(gè)鎖的場(chǎng)景我用的不多,不知道為什么企業(yè)很愛考這個(gè)筋粗,我自己開發(fā)中用的是真不多策橘。分布式鎖用的比較多。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末娜亿,一起剝皮案震驚了整個(gè)濱河市丽已,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌买决,老刑警劉巖沛婴,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異督赤,居然都是意外死亡嘁灯,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門躲舌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丑婿,“玉大人,你說我怎么就攤上這事「睿” “怎么了秒旋?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)诀拭。 經(jīng)常有香客問我迁筛,道長(zhǎng),這世上最難降的妖魔是什么耕挨? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任细卧,我火速辦了婚禮,結(jié)果婚禮上筒占,老公的妹妹穿的比我還像新娘贪庙。我一直安慰自己,他們只是感情好赋铝,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布插勤。 她就那樣靜靜地躺著,像睡著了一般革骨。 火紅的嫁衣襯著肌膚如雪农尖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天良哲,我揣著相機(jī)與錄音盛卡,去河邊找鬼。 笑死筑凫,一個(gè)胖子當(dāng)著我的面吹牛滑沧,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播巍实,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼滓技,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了棚潦?” 一聲冷哼從身側(cè)響起令漂,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎丸边,沒想到半個(gè)月后叠必,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡妹窖,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年纬朝,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片骄呼。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡共苛,死狀恐怖判没,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情俄讹,我是刑警寧澤哆致,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布绕德,位于F島的核電站患膛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏耻蛇。R本人自食惡果不足惜踪蹬,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望臣咖。 院中可真熱鬧跃捣,春花似錦、人聲如沸夺蛇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)刁赦。三九已至娶聘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間甚脉,已是汗流浹背丸升。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留牺氨,地道東北人狡耻。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像猴凹,于是被迫代替她去往敵國(guó)和親夷狰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容