一 同步代碼塊
1.為了解決並發操作可能造成的異常,java的多線程支持引入了同步監視器來解決這個問題,使用同步監視器的通用方法就是同步代碼塊。其語法如下:
synchronized(obj){
//同步代碼塊
}
其中obj就是同步監視器,它的含義是:線程開始執行同步代碼塊之前,必須先獲得對同步監視器的鎖定。任何時刻只能有一個線程可以獲得對同步監視器的鎖定,當同步代碼塊執行完成后,該線程會釋放對該同步監視器的鎖定。雖然java程序允許使用任何對象作為同步監視器,但 是同步監視器的目的就是為了阻止兩個線程對同一個共享資源進行並發訪問,因此通常推薦使用可能被並發訪問的共享資源充當同步監視器。
2.小例子
Account.java
public class Account { private String accountNo; private double balance; public Account(String accountNo,double balance){ this.accountNo=accountNo; this.balance=balance; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } public String getAccountNo() { return accountNo; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Account account = (Account) o; return accountNo.equals(account.accountNo); } @Override public int hashCode() { return accountNo.hashCode(); } }
DrawThread.java
public class DrawThread extends Thread { private Account account; private double drawAmount; public DrawThread(String name, Account account, double drawAmount) { super(name); this.account = account; this.drawAmount = drawAmount; } public void run(){ synchronized (account){ if(account.getBalance()>=drawAmount){ System.out.println(getName() + "取錢成功,吐出鈔票: " + drawAmount); try{ Thread.sleep(1); }catch(InterruptedException ex){ ex.getStackTrace(); } account.setBalance(account.getBalance()-drawAmount); System.out.println("\t余額為:"+account.getBalance()); }else{ System.out.println(getName()+"取錢失敗,余額不足"); } } } }
DrawTest.java
public class DrawTest { public static void main(String[] args){ Account acct=new Account("1234567",1000); new DrawThread("甲",acct,800).start(); new DrawThread("乙",acct,800).start(); } }
甲取錢成功,吐出鈔票: 800.0
余額為:200.0
乙取錢失敗,余額不足
3.如果將DrawThread的同步去掉:
public class DrawThread extends Thread { private Account account; private double drawAmount; public DrawThread(String name, Account account, double drawAmount) { super(name); this.account = account; this.drawAmount = drawAmount; } public void run(){ // synchronized (account){ if(account.getBalance()>=drawAmount){ System.out.println(getName() + "取錢成功,吐出鈔票: " + drawAmount); try{ Thread.sleep(1); }catch(InterruptedException ex){ ex.getStackTrace(); } account.setBalance(account.getBalance()-drawAmount); System.out.println("\t余額為:"+account.getBalance()); }else{ System.out.println(getName()+"取錢失敗,余額不足"); } // } } }
會出現這些情況的結果:
乙取錢成功,吐出鈔票: 800.0
甲取錢成功,吐出鈔票: 800.0
余額為:200.0
余額為:-600.0
甲取錢成功,吐出鈔票: 800.0
乙取錢成功,吐出鈔票: 800.0
余額為:200.0
余額為:200.0
程序使用synchronized將run()方法里的方法體修改成同步代碼塊,同步監視器就是account對象,這樣的做法符合“加鎖-修改-釋放鎖”的邏輯,這樣就可以保證並發線程在任一時刻只有一個線程進入修改共享資源的代碼區。多次運行,結果只有一個。
二 同步方法
1.同步方法就是使用synchronized關鍵字修飾某個方法,這個方法就是同步方法。這個同步方法(非static方法)無須顯式指定同步監視器,同步方法的同步監視器是this,也就是調用該方法的對象。通過同步方法可以非常方便的實現線程安全的類,線程安全的類有如下特征:
該類的對象可以方便的被多個線程安全的訪問;
每個線程調用該對象的任意方法之后都能得到正確的結果;
每個線程調用該對象的任意方法之后,該對象狀態依然能保持合理狀態。
2.不可變類總是線程安全的,因為它的對象狀態不可改變可變類需要額外的方法來保證其線程安全,在Account類中我們只需要把balance的方法變成同步方法即可。
Account.java
public class Account { private String accountNo; private double balance; public Account(String accountNo,double balance){ this.accountNo=accountNo; this.balance=balance; } //因為賬戶余額不可以隨便更改,所以只為balance提供getter方法 public double getBalance() { return balance; } public String getAccountNo() { return accountNo; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Account account = (Account) o; return accountNo.equals(account.accountNo); } @Override public int hashCode() { return accountNo.hashCode(); } //提供一個線程安全的draw()方法來完成取錢操作 public synchronized void draw(double drawAmount){ if(balance>=drawAmount){ System.out.println(Thread.currentThread().getName()+"取錢成功!吐出鈔票:"+drawAmount); try{ Thread.sleep(1); }catch (InterruptedException ex){ ex.printStackTrace(); } balance-=drawAmount; System.out.println("\t余額為:"+balance); }else{ System.out.println(Thread.currentThread().getName()+"取錢失敗,余額不足"); } } }
DrawThread.java
public class DrawThread extends Thread { private Account account; private double drawAmount; public DrawThread(String name, Account account, double drawAmount) { super(name); this.account = account; this.drawAmount = drawAmount; } public void run(){ account.draw(drawAmount); } }
DrawTest.java
public class DrawTest { public static void main(String[] args){ Account acct=new Account("1234567",1000); new DrawThread("甲",acct,800).start(); new DrawThread("乙",acct,800).start(); } }
注意,synchronized可以修飾方法,修飾代碼塊,但是不能修飾構造器、成員變量等。在Account類中定義draw()方法,而不是直接在 run()方法中實現取錢邏輯,這種做法更符合面向對象規則。DDD設計方式,即Domain Driven Design(領域驅動設計),認為每個類都應該是完備的領域對象,Account代表用戶賬戶,就應該提供用戶賬戶的相關方法。通過draw()方法來執行取錢操作,而不是直接將setBalance()方法暴露出來任人操作。
但是,可變類的線程安全是以降低程序的運行效率為代價的,不要對線程安全類的所有方法都進行同步,只對那些會改變競爭資源(共享資源)的方法進行同步。同時,可變類有兩種運行環境:單線程環境和多線程環境, 則應該為可變類提供兩種版本,即線程安全版本和線程不安全版本。如JDK提供的StringBuilder在單線程環境下保證更好的性能,StringBuffer可以保證多線程安全。
三 釋放同步監視器的鎖定
1.任何線程進入同步代碼塊,同步方法之前,必須先獲得對同步監視器的鎖定,那么如何釋放對同步監視器的鎖定呢,線程會在一下幾種情況下釋放同步監視器:
當前線程的同步方法、同步代碼塊執行結束,當前線程即釋放同步監視器;
當前線程在同步代碼塊、同步方法中遇到break,return終止了該代碼塊、方法的繼續執行;
當前線程在同步代碼塊、同步方法中出現了未處理的Error或Exception,導致了該代碼塊、方法的異常結束;
當前線程執行同步代碼塊或同步方法時,程序執行了同步監視器對象的wait()方法,則當前線程暫停,並釋放同步監視器;
2.以下幾種情況,線程不會釋放同步監視器:
線程執行同步代碼塊或同步方法時,程序調用Thread.sleep(),Thread.yield()方法來暫停當前線程的執行,當前線程不會釋放同步監視器;
線程執行同步代碼塊時,其他線程調用了該線程的suspend()方法將該線程掛起,該線程不會釋放同步監視器,當然,程序應盡量避免使用suspend()和resume()方法來控制線程。
四 同步鎖:
1.Java5開始,Java提供了一種功能更加強大的線程同步機制——通過顯式定義同步鎖對象來實現同步,這里的同步鎖由Lock對象充當。
Lock 對象提供了比synchronized方法和synchronized代碼塊更廣泛的鎖定操作,Lock是控制多個線程對共享資源進行訪問的工具。通常, 鎖提供了對共享資源的獨占訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共享資源之前應該先獲得Lock對象。
某些鎖可能允許對共享資源並發訪問,如ReadWriteLock(讀寫鎖),Lock,ReadWriteLock是Java5提供的兩個根接口,並為 Lock提供了ReentrantLock實現類,為ReadWriteLock提供了ReentrantReadWriteLock實現類。在 Java8中提供了新型的StampLock類,在大多數場景下它可以替代傳統的ReentrantReadWriteLock。 ReentrantReadWriteLock為讀寫操作提供了三種鎖模式:Writing,ReadingOptimistic,Reading。
2.在實現線程安全的控制中,比較常用的是ReentrantLock(可重入鎖)。主要的代碼格式如下:
class X{ //定義鎖對象 private final ReentrantLock lock=new ReentrantLock(); //定義需要保證線程安全的方法 public void m(){ //加鎖 lock.lock(); try{ //...method body } //使用finally塊來保證釋放鎖 finally{ lock.unlock(); } } }
將Account.java修改為:
public class Account { private final ReentrantLock lock=new ReentrantLock(); private String accountNo; private double balance; public Account(String accountNo,double balance){ this.accountNo=accountNo; this.balance=balance; } //因為賬戶余額不可以隨便更改,所以只為balance提供getter方法 public double getBalance() { return balance; } public String getAccountNo() { return accountNo; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Account account = (Account) o; return accountNo.equals(account.accountNo); } @Override public int hashCode() { return accountNo.hashCode(); } //提供一個線程安全的draw()方法來完成取錢操作 public void draw(double drawAmount){ //加鎖 lock.lock(); try { if (balance >= drawAmount) { System.out.println(Thread.currentThread().getName() + "取錢成功!吐出鈔票:" + drawAmount); try { Thread.sleep(1); } catch (InterruptedException ex) { ex.printStackTrace(); } balance -= drawAmount; System.out.println("\t余額為:" + balance); } else { System.out.println(Thread.currentThread().getName() + "取錢失敗,余額不足"); } }finally { lock.unlock(); } } }
使用Lock與使用同步代碼有點相似,只是使用Lock時可以顯式使用Lock對象作為同步鎖,而使用同步方法時系統隱式使用當前對象作為同步監視器。使用 Lock時每個Lock對象對應一個Account對象,一樣可以保證對於同一個Account對象,同一個時刻只能有一個線程進入臨界區。Lock提供 了同步方法和同步代碼塊所沒有的其他功能,包括使用非塊結構的tryLock()方法,以及試圖獲取可中斷鎖的lockInterruptibly()方法,還有獲取超時失效鎖的tryLock(long,TimeUnit)方法。
ReentrantLock可重入鎖的意思是,一個線程可以對已被加鎖的ReentrantLock鎖再次加鎖,ReentrantLock對象會維持一個計數器來追蹤lock()方法的嵌套調用,線程在每次調用lock()加鎖后,必須顯式調用unlock()來釋放鎖,所以一段被鎖保護的代碼可以調用另一個被相同鎖保護的方法。
五 死鎖
當兩個線程相互等待對方釋放同步監視器時就會發生死鎖,Java虛擬機沒有檢測,也沒有采取措施來處理死鎖情況,所以多線程編程時應該采取措施避免死鎖出現。一旦出現死鎖,程序既不會發生任何異常,也不會給出任何提示,只是所有線程都處於阻塞狀態,無法繼續。
如DeadLock.java
class A{ public synchronized void foo(B b){ System.out.println("當前線程名為:"+Thread.currentThread().getName()+"進入了A實例的foo()方法"); try{ Thread.sleep(200); }catch(InterruptedException ex){ ex.printStackTrace(); } System.out.println("當前線程名為:"+Thread.currentThread().getName()+"試圖調用B實例的last()方法"); b.last(); } public synchronized void last(){ System.out.println("進入了A類的last()方法內部"); } } class B{ public synchronized void bar(A a){ System.out.println("當前線程名為:"+Thread.currentThread().getName()+"進入了B實例的bar()方法"); try{ Thread.sleep(200); }catch(InterruptedException ex){ ex.printStackTrace(); } System.out.println("當前線程名為:"+Thread.currentThread().getName()+"試圖調用A實例的last()方法"); a.last(); } public synchronized void last(){ System.out.println("進入了B類的last()方法內部"); } } public class DeadLock implements Runnable{ A a =new A(); B b=new B(); public void init(){ Thread.currentThread().setName("主線程"); a.foo(b); System.out.println("進入了主線程之后..."); } public void run(){ Thread.currentThread().setName("副線程"); b.bar(a); System.out.println("進入了副線程之后..."); } public static void main(String[] args){ DeadLock d1=new DeadLock(); new Thread(d1).start(); d1.init(); } }
結果:
當前線程名為:主線程進入了A實例的foo()方法
當前線程名為:副線程進入了B實例的bar()方法
當前線程名為:主線程試圖調用B實例的last()方法
當前線程名為:副線程試圖調用A實例的last()方法
