基本同步
同步概述
多個線程共享一個資源的場景非常常見,比如多個線程讀或者寫相同的數據等。爲了防止共享資源可能出現的錯誤或數據不一致,需要有一些機制來防止錯誤的發生。
臨界區(Critical Section)是指訪問共享資源的代碼塊,這個代碼塊在同一時間只允許一個線程執行。Java提供了同步機制來實現臨界區。當一個線程試圖訪問一個臨界區時,它使用同步機制查看是不是已經有其他線程進入了臨界區,如果美歐它就可以進入臨界區;如果已經有線程進入臨界區,它就被同步機制掛起,直到進入的線程離開這個臨界區。如果在等待進入臨界區的線程不止一個,JVM會選擇其中的一個,其餘將繼續等待。
Java中的同步機制有兩種:
- synchronized關鍵字機制
- Lock接口及其實現(JDK5.0引入)
本節討論synchronized機制,這是最基本最常用的同步機制。後面的章節討論Lock機制。
synchronized
JAVA中最基本的同步方式就是使用synchronized關鍵字。每一個使用synchronized關鍵字聲明的方法都是臨界區,一個對象的臨界區在同一時間只有一個允許被訪問。synchronized關鍵字也可以聲明在靜態方法上,用synchronized聲明的靜態方法同時只能夠被一個執行線程訪問,但是其他線程可以訪問這個對象的非靜態方法。這點要特別注意,因爲兩個線程可以同時訪問一個對象的兩個方法,一個是靜態的,另一個是非靜態的。如果兩個方法都改變了相同的數據,將會出現數據不一致的錯誤。
synchronized是可重入的。
下面以銀行轉賬的經典示例來說明synchronized關鍵字的用法,這個示例使用兩個線程訪問同一個賬戶對象,一個線程轉錢到此賬戶,一個線程從此賬戶中取錢,如果沒有同步機制那麼最後賬戶的餘額是不對的,同步機制保證賬戶的餘額最終是正確的,下面的示例將在轉入和轉出時使用synchronized關鍵子進行同步。其實更本質的是在修改賬戶餘額時需要同步。
public class AccoutSynchronized {
public static void main(String[] args){
final Account account = new Account(1000);
System.out.println("main:初始賬戶。賬戶餘額:" + account.getBalance());
System.out.println("main:創建工作線程。");
Thread addThread = new Thread(new AddWorker(account));
Thread subThread = new Thread(new SubWorker(account));
System.out.println("main:啓動工作線程。");
addThread.start();
subThread.start();
try {
System.out.println("main:等待工作線程執行完成。");
addThread.join();
subThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("main:工作線程執行完成。賬戶餘額:" + account.getBalance());
System.out.println("main:退出。");
}
}
/** 轉入線程**/
class AddWorker implements Runnable{
private Account account;
public AddWorker(Account account){
this.account = account;
}
@Override
public void run(){
System.out.println("轉入工作線程啓動。");
for(int i=0; i<100; i++){
account.addAmount(1000);
}
System.out.println("轉入工作線程完成。");
}
}
/** 轉出線程**/
class SubWorker implements Runnable{
private Account account;
public SubWorker(Account account){
this.account = account;
}
@Override
public void run(){
System.out.println("轉出工作線程啓動。");
for(int i=0; i<100; i++){
account.subAmount(1000);
}
System.out.println("轉出工作線程完成。");
}
}
/** 賬戶 **/
class Account{
private double balance;
public Account(double balance){
this.balance = balance;
}
/** 轉入賬戶 */
public synchronized void addAmount(double amount){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance += amount;
}
/** 轉出賬戶 */
public synchronized void subAmount(double amount){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
}
public double getBalance(){
return this.balance;
}
}
程序執行日誌如下:
main:初始賬戶。賬戶餘額:1000.0
main:創建工作線程。
main:啓動工作線程。
main:等待工作線程執行完成。
轉入工作線程啓動。
轉出工作線程啓動。
轉出工作線程完成。
轉入工作線程完成。
main:工作線程執行完成。賬戶餘額:1000.0
main:退出。