Java中可以使用關鍵字synchronized進行線程同步控制,實現關鍵資源順序訪問,避免由於多線程並發執行導致的資料不一致性等問題。synchronized的原理是對象監視器(鎖),只有擷取到監視器的線程才能繼續執行,否則線程會等待擷取監視器。Java中每個對象或者類都有一把鎖與之相關聯,對於對象來說,監視的是這個對象的執行個體變數,對於類來說,監視的是類變數(一個類本身是類Class的對象,所以與類關聯的鎖也是對象鎖)。synchronized關鍵字使用方式有兩種:synchronized方法和synchronized塊。這兩種監視地區都和一個引入對象相關聯,當到達這個監視地區時,JVM就會鎖住這個引用對象,當離開時會釋放這個引用對象上的鎖(有異常退出時,JVM會釋放鎖)。對象鎖是JVM內部機制,只需要編寫同步方法或者同步塊即可,操作監視地區時JVM會自動擷取鎖或者釋放鎖。
樣本1
先來看第一個樣本,在java中,同一個對象的臨界區,在同一時間只有一個允許被訪問(都是非靜態synchronized方法):
package concurrency;public class Main8 { public static void main(String[] args) { Account account = new Account(); account.setBalance(1000); Company company = new Company(account); Thread companyThread = new Thread(company); Bank bank = new Bank(account); Thread bankThread = new Thread(bank); System.out.printf("Account : Initial Balance: %f\n", account.getBalance()); companyThread.start(); bankThread.start(); try { //join()方法等待這兩個線程運行完成 companyThread.join(); bankThread.join(); System.out.printf("Account : Final Balance: %f\n", account.getBalance()); } catch (InterruptedException e) { e.printStackTrace(); } }}
/*帳戶*/class Account{ private double balance; /*將傳入的資料加到餘額balance中*/ public synchronized void addAmount(double amount){ double tmp = balance; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } tmp += amount; balance = tmp; } /*將傳入的資料從餘額balance中扣除*/ public synchronized void subtractAmount(double amount){ double tmp = balance; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } tmp -= amount; balance = tmp; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; }}
/*銀行*/class Bank implements Runnable{ private Account account; public Bank(Account account){ this.account = account; } @Override public void run() { for (int i = 0; i < 100; i++) { account.subtractAmount(1000); } }}
/*公司*/class Company implements Runnable{ private Account account; public Company(Account account){ this.account = account; } @Override public void run() { for (int i = 0; i < 100; i++) { account.addAmount(1000); } }}
你已經開發了一個銀行賬戶的類比應用,它能夠對餘額進行儲值和扣除。這個程式通過調用100次addAmount()方法對帳戶進行儲值,每次存入1000;然後通過調用100次subtractAmount()方法對帳戶餘額進行扣除,每次扣除1000;我們期望帳戶的最終餘額與起初餘額相等,我們通過synchronized關鍵字實現了。
如果想查看共用資料的並發訪問問題,只需要將addAmount()和subtractAmount()方法聲明中的synchronized關鍵字刪除即可。在沒有synchronized關鍵字的情況下,列印出來的餘額值並不一致。如果多次運行這個程式,你將擷取不同的結果。因為JVM並不保證線程的執行順序,所以每次啟動並執行時候,線程將以不同的順序讀取並且修改帳戶的餘額,造成最終結果也是不一樣的。
一個對象的方法採用synchronized關鍵字進行聲明,只能被一個線程訪問。如果線程A正在執行一個同步方法syncMethodA(),線程B要執行這個對象的其他同步方法syncMethodB(),線程B將被阻塞直到線程A訪問完。但如果線程B訪問的是同一個類的不同對象,那麼兩個線程都不會被阻塞。
樣本2
示範同一個對象上的靜態synchronized方法與非靜態synchronized方法可以在同一時間點被多個線程訪問的問題,驗證一下。
package concurrency;public class Main8 { public static void main(String[] args) { Account account = new Account(); account.setBalance(1000); Company company = new Company(account); Thread companyThread = new Thread(company); Bank bank = new Bank(account); Thread bankThread = new Thread(bank); System.out.printf("Account : Initial Balance: %f\n", account.getBalance()); companyThread.start(); bankThread.start(); try { //join()方法等待這兩個線程運行完成 companyThread.join(); bankThread.join(); System.out.printf("Account : Final Balance: %f\n", account.getBalance()); } catch (InterruptedException e) { e.printStackTrace(); } }}
/*帳戶*/class Account{ /*這裡改為靜態變數*/ private static double balance = 0; /*將傳入的資料加到餘額balance中,注意是用static修飾過的*/ public static synchronized void addAmount(double amount){ double tmp = balance; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } tmp += amount; balance = tmp; } /*將傳入的資料從餘額balance中扣除*/ public synchronized void subtractAmount(double amount){ double tmp = balance; try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } tmp -= amount; balance = tmp; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; }}
/*銀行*/class Bank implements Runnable{ private Account account; public Bank(Account account){ this.account = account; } @Override public void run() { for (int i = 0; i < 100; i++) { account.subtractAmount(1000); } }}
/*公司*/class Company implements Runnable{ private Account account; public Company(Account account){ this.account = account; } @Override public void run() { for (int i = 0; i < 100; i++) { account.addAmount(1000); } }}
我只是把上個例子中的,balance加了static關鍵字修改,addAmount()方法也可以static關鍵字修飾。執行結果大家可以自己測試一下,每次執行都是不一樣的結果!
一些總結:
- synchronized是通過軟體(JVM)實現的,簡單易用,即使在JDK5之後有了Lock,仍然被廣泛地使用。
- synchronized實際上是非公平的,新來的線程有可能立即獲得監視器,而在等待區中等候已久的線程可能再次等待,不過這種搶佔的方式可以預防饑餓。
- synchronized只有鎖只與一個條件(是否擷取鎖)相關聯,不靈活,後來Condition與Lock的結合解決了這個問題。
- 多線程競爭一個鎖時,其餘未得到鎖的線程只能不停的嘗試獲得鎖,而不能中斷。高並發的情況下會導致效能下降。ReentrantLock的lockInterruptibly()方法可以優先考慮響應中斷。 一個線程等待時間過長,它可以中斷自己,然後ReentrantLock響應這個中斷,不再讓這個線程繼續等待。有了這個機制,使用ReentrantLock時就不會像synchronized那樣產生死結了。