Java核心技術 併發

一個程序同時執行多個任務,每個任務稱爲一個線程(thread),它是線程控制的簡稱。可以同時運行一個以上線程的程序稱爲多線程程序(multithreaded)。
多線程和多進程的區別:本質區別在於每個進程都擁有自己的一套變量,而線程則共享數據。共享變量使線程之間的通信比進程之間的通信更有效、更容易。在有些操作系統中,與進程相比,線程更加輕量級,創建、撤銷一個線程比啓動新進程的開銷要小得多。

1.什麼是線程

在這裏插入圖片描述
當點擊Start按鈕時,程序將從屏幕左上角彈出一個球,Start按鈕的處理程序將調用addBall方法。這個方法循環運行1000次move。每調用一次move,球就會移動一點,當碰到牆壁時,球將調整方法,並重新繪製面板。
調用Thread.sleep不會創建新線程,sleep是Thread類的靜態方法,用於暫停當前線程的活動。sleep方法可能會拋出一個InterruptedException方法。

public class Bounce {
    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            JFrame frame = new BounceFrame();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }
}
class BounceFrame extends JFrame {
    private BallComponent comp;
    public static final int STEPS = 1000;
    public static final int DELAY = 3;
    public BounceFrame() {
        setTitle("Bounce");
        comp = new BallComponent();
        add(comp, BorderLayout.CENTER);
        JPanel buttonPanel = new JPanel();
        addButton(buttonPanel, "Start", e -> addBall());
        addButton(buttonPanel, "Close", e -> System.exit(0));
        add(buttonPanel, BorderLayout.SOUTH);
        pack();
    }
    public void addButton(Container c, String title, ActionListener listener) {
        JButton button = new JButton(title);
        c.add(button);
        button.addActionListener(listener);
    }
    private void addBall() {
        try {
            Ball ball = new Ball();
            comp.add(ball);
            for (int i = 1; i <= STEPS; i++) {
                ball.move(comp.getBounds());
                comp.paint(comp.getGraphics());
                Thread.sleep(DELAY);
            }
        } catch (InterruptedException e) {}
    }
}
public class Ball {
    private static final int XSIZE = 15;
    private static final int YSIZE = 15;
    private double x = 0;
    private double y = 0;
    private double dx = 1;
    private double dy = 1;

    public void move(Rectangle2D bounds) {
        x += dx;
        y += dy;
        if (x < bounds.getMinX()) {
            x = bounds.getMinX();
            dx = -dx;
        }
        if (x + XSIZE >= bounds.getMaxX()) {
            x = bounds.getMaxX() - XSIZE;
            dx = -dx;
        }
        if (y < bounds.getMinY()) {
            y = bounds.getMinY();
            dy = - dy;
        }
        if (y + YSIZE >= bounds.getMaxY()) {
            y = bounds.getMaxY() - YSIZE;
            dy = -dy;
        }
    }

    public Ellipse2D getShape() {
        return new Ellipse2D.Double(x, y, XSIZE, YSIZE);
    }
}
public class BallComponent extends JPanel {
    private static final int DEFAULT_WIDTH = 450;
    private static final int DEFAULT_HEIGHT = 350;

    private java.util.List<Ball> balls = new ArrayList<>();

    public void add(Ball b) {
        balls.add(b);
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;
        for (Ball b : balls) {
            g2.fill(b.getShape());
        }
    }

    @Override
    public Dimension getPreferredSize() {
        return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
    }
}

這個程序控制了整個過程。如果在球完成1000次之前點擊Close按鈕,會發現球仍在彈跳。在球自己結束彈跳之前無法與程序進行交互。
顯然這個程序性能相當糟糕。肯定不能讓程序用這種方式完成一個非常耗時的工作(網絡連接讀取數據時,阻塞其他任務是經常發生的,有時確實想要中斷讀取操作)。

使用線程給其他任務提供機會

可以將移動球的代碼放置在一個獨立的線程中,運行這段代碼可以提高球的響應能力。
AWT的事件分派線程將一直並行運行,以處理用戶界面的事件。由於每個線程都有機會得以運行,所以在球彈跳過程中,當用戶點擊close按鈕時,時間調度線程將有機會關注這個事件,已處理關閉動作。
一個單獨線程中執行一個任務的簡單過程:
1.將任務代碼移到實現了Runnable接口的類的run方法中。這個接口只有一個方法:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

由於Runnable是一個函數式接口,可以用lambda表達式建立一個實例:
Runnable r = () -> { task code };
2.由Runnable創建一個Thread對象:
Thread t = new Thread®;
3.啓動線程:
t.start();

無論何時點擊Start按鈕,球會移入一個新的線程。
在這裏插入圖片描述

public class BounceThread {
    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            JFrame frame = new BounceFrameThread();
            frame.setTitle("BounceThread");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        });
    }
}
class BounceFrameThread extends JFrame {
    private BallComponent comp;
    public static final int STEPS = 1000;
    public static final int DELAY = 5;

    public BounceFrameThread() {
        comp = new BallComponent();
        add(comp, BorderLayout.CENTER);
        JPanel buttonPanel = new JPanel();
        addButton(buttonPanel, "Start", e -> addBall());
        addButton(buttonPanel, "Close", e -> System.exit(0));
        add(buttonPanel, BorderLayout.SOUTH);
        pack();
    }

    public void addButton(Container c, String title, ActionListener listener) {
        JButton button = new JButton(title);
        c.add(button);
        button.addActionListener(listener);
    }

    private void addBall() {
        Ball ball = new Ball();
        comp.add(ball);

        Runnable r = () -> {
            try {
                for (int i = 1; i <= STEPS; i++) {
                    ball.move(comp.getBounds());
                    comp.repaint();
                    Thread.sleep(DELAY);
                }
            } catch (InterruptedException e) {}
        };
        Thread t = new Thread(r);
        t.start();
    }
}

也可以通過構建Thread類的子類定義一個線程:

class MyThread extends Thread {
	public void run(){
		...
	}
}

構造一個子類對象,並調用start方法,不過這種方法不再推薦。應該將要並行運行的任務與運行機制解耦合。如果有很多任務,爲每個任務創建一個獨立的線程所付出的代價太大。可以使用線程池解決問題。
不要調用Thread類或Runnable類的run方法。直接調用run方法,只會執行同一個線程中的任務,而不會啓動新線程。應該調用Thread.start方法,這個方法將創建一個執行run方法的新線程。

2.中斷線程

當線程的run方法執行方法體中最後一條語句後,並經由執行return語句返回時,或者出現了在方法中沒有捕獲的異常時,線程將終止。在Java早期版本中有一個stop方法,其他線程可以調用它終止線程,但是現在已經被棄用了。
沒有可以強制終止線程的方法。interrupt方法可以請求終止線程。
當對線程調用interrupt方法時,線程的中斷狀態將被置位。這是每一個線程都具有的boolean標誌。每個線程都應該不時地檢查這個標誌,以判斷線程是否被中斷。

// 檢測中斷狀態是否被置位
while (!Thread.currentThread().isInterrupted()){
	//do more work
}

如果線程被阻塞,就無法檢測中斷狀態。當一個被阻塞的線程(sleep或wait)上調用interrupt方法時,阻塞調用將會被InterruptedException異常中斷。
中斷一個線程只是爲了引起它的注意。被中斷的線程可以決定如何響應中斷。某些線程是如此重要,以至於應該處理完異常後,繼續執行,而不理會中斷。普遍情況是,線程將簡單地將中斷作爲一個終止的請求:

Runnable r = () -> {
	try {
		...
		while (!Thread.currentThread().isInterrupted() /**&& more work to do*/) {
			//do more work
		}
	} catch (InterruptedException e) {
		// 線程被sleep或wait中斷
	} finally {
		//cleanup,if required
	}
	// 退出run方法終止線程
};

每次工作之後都調用sleep方法(或者其他的可中斷方法),isInterrupted檢測既沒有必要也沒有用處。如果在中斷狀態被置位時調用sleep方法,它不會休眠。相反,它將清除這一狀態並拋出InterruptedException。因此如果循環調用sleep,不會檢測中斷狀態。相反,要捕獲InterruptedException異常:

Runnable r = () -> {
	try {
		...
		while (/**more work to do*/) {
			//do more work
			Thread.sleep(delay);
		}
	} catch (InterruptedException e) {
		// 線程被sleep中斷
	} finally {
		//cleanup,if required
	}
	// 退出run方法終止線程
};

靜態方法interrupted檢測當前線程是否被中斷,而且會清除該線程的中斷狀態。
isInterrupted是一個實例方法,可用來檢測是否有線程被中斷,不會改變中斷狀態。
在很多代碼中會發現InterruptedException異常被抑制在很低的層次上:

void mySubTask() {
	...
	try {sleep(delay);}
	catch (InterruptedException e) {} //不要忽略
}

如果不認爲在catch子句中做這一處理有什麼好處的話,有兩種合理的選擇:
1.在catch子句中調用Thread.currentThread().interrupted()來設置中斷狀態,調用者可以對其進行檢測:

void mySubTask() {
	...
	try {sleep(delay);}
	catch (InterruptedException e) {
		Thread.currentThread().interrupted();
	}
}

2.更好的選擇,拋出InterruptedException異常,調用者(最終的run方法)可以捕獲這一異常:

void mySubTask() throws InterruptedException {
	...
	sleep(delay);
	...
}

3.線程狀態

線程有6種狀態,可調用getState方法確定一個線程的當前狀態:
New(新創建)
Runnable(可運行)
Blocaked(被阻塞)
Waiting(等待)
Timed waiting(計時等待)
Terminated(被終止)

新創建線程

當用new操作符創建一個新線程時,如new Thread®,該線程還沒有開始運行。意味着它的狀態是new。
當一個線程處於新創建狀態時,程序還沒有開始運行線程中的代碼。在線程運行之前還有一些基礎工作要做。

可運行線程

一旦調用start方法,線程處於runnable狀態。一個可運行的程序可能正在運行也可能沒有運行,這取決於操作系統給線程提供的運行時間(一個正在運行中的線程仍處於可允許狀態)。
運行中的線程可能被中斷,爲了讓其他線程獲得運行機會。線程調度的細節依賴於操作系統提供的服務。
搶佔式調度系統給每一個可運行線程一個時間片來執行任務。當時間片用完,操作系統剝奪該線程的運行權,並給另一個線程機會。當先一個線程時,操作系統考慮線程的優先級。
所有的桌面以及服務器操作系統都是用搶佔式調度。但是,像手機這樣的消息設備可能使用協作式調度,一個線程只有在調用yield方法、或者被阻塞或等待時,線程才失去控制權。

被阻塞線程和等待線程

當線程處於被阻塞或等待狀態時,它暫時不活動,不允許任何代碼且消耗最少的資源。直到線程調度器重新激活它。細節取決於它是怎樣達到非活動狀態的:
1.當一個線程試圖獲取一個內部的對象鎖,而該鎖被其他線程持有,則該線程進入阻塞狀態。當所有其他線程釋放該鎖,並且線程調度器允許本線程持有它的時候,該線程將變成非阻塞狀態。
2.當線程等待另一個線程通知調度器一個條件時,它自己進入等待狀態。在調用Object.wait或Thread.join方法,或者是等待java.util.concurrent庫中的Lock或Condition時,就會出現這種情況。實際上,被阻塞狀態和等待狀態有很大不同
3.有幾個方法有一個超時參數(Thread.sleep、Object.wait、Thread.join、Lock.tryLock、Condition.await的計時版)。調用它們導致線程進入計時等待狀態。這一狀態將一直保持到超時期滿或接收到適當的通知。
在這裏插入圖片描述

被終止的線程

兩個原因:
1.因爲run方法正常退出而自然死亡
2.因爲一個沒有捕獲的異常終止了run方法而意外死亡
特別是,可以調用線程的stop方法殺死一個線程。該方法拋出ThreadDeath錯誤對象,由此殺死線程。但是,stop方法已過時,不要在調用它。

4.線程屬性

線程優先級

Java中,每一個線程有一個優先級。默認情況下,一個線程繼承它的父線程的優先級。可以用setPriority方法提高或降低任何一個線程的優先級。可以將優先級設置爲在MIN_PRIORITY(1)與MAX_PRIORITY(10)之間的任何值。NORM_PRIORITY被定義爲5
當虛擬機依賴於宿主主機平臺的線程實現機制時,Java線程的優先級被映射到宿主機平臺的優先級上,優先級個數也許更多,也許更少。
Windows有7個優先級,一些Java優先級將映射到相同的操作系統優先級。Oracle爲Linux提供的Java虛擬機中,線程優先級被忽略——所有線程具有相同的優先級。
如果有幾個高優先級的線程沒有進入非活動狀態,低優先級的線程可能永遠不能運行(完全餓死)。

守護線程

t.setDaemon(true),將線程轉換爲守護線程(daemon thread)。
守護線程的唯一用途是爲其他線程提供服務。
當只剩下守護線程時,虛擬機就會退出,由於如果只剩下守護線程,就沒有必要繼續運行程序了。

未捕獲異常處理器

線程的run方法不能拋出任何受檢異常,但是非受檢異常會導致線程終止。在這種情況下,線程就會死亡。
但是不需要任何catch子句來處理可以被傳播的異常,相反就在線程死亡之前,異常被傳遞到一個用於未捕獲異常的處理器。該處理器必須屬於一個實現Thread.UncaughtExceptionHandler接口的類。這個接口只有一個方法:void uncaughtException(Thread t, Throwable e)。
可以用setUncaughtExceptionHandler方法爲任何線程安裝一個處理器。也可以用Thread類的靜態方法setDefaultUncaughtExceptionHandler爲所有線程安裝一個默認處理器。替換處理器可以使用日誌API發送未捕獲異常的報告到日誌文件。
如果不安裝默認處理器,默認的處理器爲空。但是如果不爲獨立的線程安裝處理器,此時的處理器就是該線程的ThreadGroup對象。
ThreadGroup線程組是一個可以統一管理的線程集合。默認情況下,所有線程都屬於相同的線程組(也可能會建立其他的組)。現在引入了更好的特性用於線程集合操作,所有建議不要使用線程組。
ThreadGroup類實現Thread.UncaughtExceptionHandler接口,它的uncaughtException方法:
1.如果該線程組有父線程組,那麼父線程組的uncaughtException方法被調用
2.否則,如果Thread.setDefaultUncaughtExceptionHandler方法返回一個非空處理器,則調用該處理器
3.否則,如果Throwable是ThreadDeath的一個實例,什麼都不做
4.否則,線程的名字以及Throwable的棧軌跡輸出到System.err上

5.同步

競爭條件的一個例子

爲了避免多線程引起的對共享數據的訛誤(競爭條件(race condition)),必須知道如何同步存取。
模擬一個有若干賬戶的銀行,隨機地生成在這些賬戶之間轉移錢款的交易。每個賬戶有一個線程。每筆交易中,會從線程所服務的賬戶隨機轉移一定的數目的錢款到另一個賬戶:

public class Bank {
    private final double[] accounts;

    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        Arrays.fill(accounts, initialBalance);
    }

    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());
    }

    public double getTotalBalance() {
        double sum = 0;

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

    public int size() {
        return accounts.length;
    }
}
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 = () -> {
              try {
                  while (true) {
                      int toAccount = (int) (bank.size() * Math.random());
                      double amount = MAX_AMOUNT * Math.random();
                      bank.transfer(fromAccount, toAccount, amount);
                      Thread.sleep((int) (DELAY * Math.random()));
                  }
              } catch (InterruptedException e){}
            };
            Thread t = new Thread(r);
            t.start();
        }
    }
}
//Thread[Thread-11,5,main]     272.18 from 11 to 69 Total Balance:  100000.00
//Thread[Thread-88,5,main]     828.72 from 88 to 92 Total Balance:  100000.00
//Thread[Thread-3,5,main]     966.22 from 3 to 47 Total Balance:  100000.00
//Thread[Thread-35,5,main]Thread[Thread-68,5,main]     238.30 from 68 to 63 Total Balance:   99176.76
//Thread[Thread-23,5,main]     279.66 from 23 to 21 Total Balance:   99176.76
//Thread[Thread-69,5,main]     282.43 from 69 to 82 Total Balance:   99176.76
//Thread[Thread-70,5,main]     297.22 from 70 to 12 Total Balance:   99176.76
//Thread[Thread-50,5,main]     152.47 from 50 to 39 Total Balance:   99176.76

這個模擬程序運行時,不清楚在某個時刻某一個賬戶中錢有多少,但是知道所有賬戶總金額會保持不變。
從結果看,出現了錯誤,餘額總量有輕微的變化。

競爭條件詳解

假定兩個線程同時執行指令account[to] += amount,該語句不是原子性操作,該指令可能被處理如下:
1.將accounts[to]加載到寄存器
2.增加amount
3.將結果寫回到accounts[0]
假定第一個線程執行了步驟1,2,然後被剝奪了運行權。假定第2個線程被喚醒並修改了accounts數組中的同一項。然後第1個線程被喚醒並完成第三步。這一動作擦去了第二個線程所做的更新,於是總金額不再正確。
在這裏插入圖片描述
類中的每一個語句在虛擬機中被翻譯成字節碼,運行命令javap -c -v Bank,對Bank.class文件進行反編譯,accounts[to] += amount被轉換爲下面字節碼:

aload_0
getfield      #2                  // Field accounts:[D
iload_2
dup2
daload
dload_3
dadd
dastore

增值命令是有幾條指令組成的,執行它們的線程可以在任何一條指令點上被中斷。
這裏通過將打印語句和更新餘額的語句交織在一起執行,增加了發生這種情況的機會。如果刪除打印語句,訛誤的風險會降低一點,因爲每個線程在再次睡眠之前所做的工作很少,調度器在計算過程中剝奪線程的運行權可能性很小。但是訛誤風險並沒有消失。

鎖對象

有兩種機制防止代碼塊受併發訪問的干擾。Java語言提供了一個synchronized關鍵字達到這一目的,並且Java SE 5.0引入了ReentrantLock類。
synchronized關鍵字自動提供一個鎖以及相關的條件,對於大多數需要顯式鎖的情況,這很便利。
java.util.concurrent框架爲這些基礎機制提供了獨立的類。

public class Bank {
    private Lock bankLock = new ReentrantLock();
   	...
    public void transfer(int from, int to, double amount) {
        bankLock.lock();
        try {
            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();
        }
    }
	...
}

用ReentrantLock保護代碼塊的基本結構,確保任何時刻只有一個線程進入臨界區。一旦一個線程封鎖了鎖對象,其他任何線程都無法通過lock語句。當其他線程調用lock時,它們被阻塞,直到第一個線程釋放鎖對象。
解鎖操作放在finally子句至關重要,如果在臨界區代碼拋出異常,鎖必須被釋放,否則其他線程將永遠阻塞。不要使用帶資源的try語句,首先解鎖方法名字不是close。即使將它重命名,帶資源的try語句也無法正常工作。它的收不希望聲明一個新變量,但是如果使用一個鎖,可能想使用多個線程共享這個變量(而不是新變量)。

每個Bank對象有自己的ReentrantLock對象。如果兩個線程試圖訪問同一個Bank對象,那麼鎖以串行方式提供服務。但是如果兩個線程訪問不同的Bank對象,每個線程得到不同的鎖對象,兩個線程都不會阻塞。
鎖是可重入的,因爲線程可以重複地獲得已持有的鎖。鎖保持一個持有計數來跟蹤對lock方法的嵌套調用。線程在每一次調用lock都要調用unlock釋放鎖。由於這一特性,被一個鎖保護的代碼,可以調用另一個使用相同鎖的方法。

條件對象

進入臨界區,卻發現某一條件滿足之後它才能執行。要使用Java庫中條件對象(條件變量(conditional variable))來管理那些已經獲得了一個鎖但卻不能做有用工作的線程。

bankLock.lock();
try{
	while (accounts[from] < amount){
		// wait
		...
	}
} finally {
	bankLock.unlock();
}

當賬戶沒有足夠的餘額,等到直到另一個線程向賬戶中注入資金。但是,這一線程剛剛獲得對bankLocak的排它性訪問,因此別的線程沒有進行存款操作的機會,這裏需要使用條件對象。
一個鎖對象可以有一個或多個相關的條件對象。可以用newCondition方法獲得一個條件對象。習慣上給每一條件對象命名爲可以反應它所表達的條件的名字:

class Bank {
	private Condition sufficientFunds;
	...
	public Bank(){
		...
		sufficientFunds = bankLock.newCondition();
	}
}

如果transfer方法發現餘額不足,調用sufficientFunds.await(),當前線程被阻塞了,並放棄了鎖(希望可以使得另一個線程可以進行增加賬戶餘額操作)。
等待獲得所得線程和調用await方法的線程存在本質不同,一旦一個線程調用await方法,它進入該條件的等待集。當鎖可用時,該線程不能馬上解除阻塞。相反,它處於阻塞狀態,直至另一個線程調用統一條件上的signalAll方法時爲止。
當另一個線程轉賬時,它應該調用sufficientFunds.signalAll(),這一調用重新激活因爲這一條件而等到的所有線程。當這些線程從等待集當中移出時,它們再次成爲可運行的,調度器將再次激活它們。同時,它們將試圖重新進入該對象。一旦鎖成爲可用的,它們中的某個將從await調用返回,獲得該鎖並從被阻塞的地方繼續執行。
此時,線程應該再次測試該條件。由於無法確保該條件被滿足——signalAll方法僅僅是通知正在等待的線程:此時有可能已經滿足條件,值得再次去檢測該條件。

通常,對await的調用應該在下面循環體中:

while(!(ok to proceed)){
	condition.await();
}

當一個線程調用await時,它沒有辦法重新激活自身。它寄希望於其他線程。如果沒有其他線程來重新激活等待的線程,它就永遠不再運行了。這將導致死鎖(deadlock)現象。
如果所有其他線程被阻塞,最後一個活動線程在解除其他線程的阻塞狀態之前就調用await方法,那麼它也被阻塞。沒有任何程序可以解除其他程序的阻塞,那麼該程序就掛起了。
經驗上講,在對象的狀態有利於等待線程的方法改變時調用signalAll。例如,當一個賬戶餘額發生變化時,等待線程會有機會檢查餘額。

還有一個方法signal,則是隨機解除等待集中某個線程的阻塞狀態。這比解除所有線程的阻塞更加有效,但也存在危險。如果隨機選擇的線程發現自己仍不能運行,那麼它再次被阻塞。如果沒有其他線程再次調用signal,那麼系統就死鎖了。
當一個線程擁有某個條件鎖時,他僅僅可以在該條件時調用await、signalAll或signal方法。

public class Bank {
    private final double[] accounts;
    private Lock bankLock;
    private Condition sufficientFunds;

    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        Arrays.fill(accounts, initialBalance);
        bankLock = new ReentrantLock();
        sufficientFunds = bankLock.newCondition();
    }

    public void transfer(int from, int to, double amount) throws InterruptedException {
        bankLock.lock();
        try {
            while (accounts[from] < amount) {
                sufficientFunds.await();
            }
            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());
            sufficientFunds.signalAll();
        } finally {
            bankLock.unlock();
        }
    }

    public double getTotalBalance() {
        bankLock.lock();
        try {
            double sum = 0;

            for (double a: accounts) {
                sum += a;
            }
            return sum;
        } finally {
            bankLock.unlock();
        }

    }

    public int size() {
        return accounts.length;
    }
}

synchronized關鍵字

有關鎖和條件的關鍵之處:
1.鎖用來保護代碼片段,任何時刻只能有一個線程執行被保護的代碼
2.鎖可以管理試圖進入被保護代碼段的線程
3.鎖可以擁有一個或多個相關的條件對象
4.每個條件對象管理那些已經進入被保護的代碼段但還不能運行的線程
Lock和Condition接口爲程序設計人員提供了高度的鎖定控制。然而大多數情況下,並不需要,並且可以使用一種嵌入到Java內部的機制。從1.0版本開始,Java中的每一個對象都有一個內部鎖。如果一個方法用synchronized關鍵字聲明,那麼對象的鎖將保護整個方法。也就是說,要調用該方法,線程必須獲得內部的對象鎖。

public synchronized void method(){
	// method body
}
// 等價於
public void method() {
	this.intrinsicLock.lock();
	try {
		// method body
	} finally {this.intrinsicLock.unlock();}
}

可以簡單地聲明Bank類的transfer方法爲synchronized,而不是使用一個顯式的鎖。
內部對象鎖只有一個相關條件。wait方法添加一個線程到等待集中,notifyAll/notify方法解除等待線程的阻塞狀態(等價於await和signalAll,wait和notifyAll是Object類的final方法,Condition方法必須被重新命名,以便不會發生衝突)。

用Java實現Bank類:

public class Bank {
    private final double[] accounts;
    public Bank(int n, double initialBalance) {
        accounts = new double[n];
        Arrays.fill(accounts, initialBalance);
    }
    
    public synchronized void transfer(int from, int to, double amount) throws InterruptedException {
        while (accounts[from] < amount) {
            wait();
        }
        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());
        notifyAll();
    }

    public synchronized double getTotalBalance() {
        double sum = 0;

        for (double a: accounts) {
            sum += a;
        }
        return sum;
    }
    
    public int size() {
        return accounts.length;
    }
}

將靜態方法聲明爲synchronized也是合法的。如果調用這個方法,該方法獲得相關的類對象的內部鎖。例如,Bank有一個靜態同步方法,那麼當該方法被調用時,Bank.class對象的鎖被鎖住。因此,沒有其他線程可以調用同一個類的這個或任何其他的同步靜態方法。

內部鎖和條件存在一些侷限:
1.不能中斷一個正在試圖獲得鎖的線程
2.試圖獲得鎖時不能設定超時
3.每個鎖僅有一個單一條件

那麼Lock和Condition對象與同步方法,應該使用那一種:
1.最好都不使用。在許多情況下,可以使用java.util.concurrent包中的一種機制,它會處理所有的加鎖。在後面會介紹使用阻塞隊列來完成一個共同任務的線程,還要研究並行流。
2.如果synchronized適合程序,那麼儘量使用它,這樣可以減少編寫代碼的數量,減少出錯的機率。
3.如果特別需要使用Lock和Condition結構提供的獨有特性時,才使用Lock和Condition。

發佈了233 篇原創文章 · 獲贊 22 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章