本文主要接着前面多線程的兩篇文章總結Java多線程中的線程安全問題。html
一.一個典型的Java線程安全例子java
public class ThreadTest { public static void main(String[] args) { Account account = new Account("123456", 1000); DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700); Thread myThread1 = new Thread(drawMoneyRunnable); Thread myThread2 = new Thread(drawMoneyRunnable); myThread1.start(); myThread2.start(); } } class DrawMoneyRunnable implements Runnable { private Account account; private double drawAmount; public DrawMoneyRunnable(Account account, double drawAmount) { super(); this.account = account; this.drawAmount = drawAmount; } public void run() { if (account.getBalance() >= drawAmount) { //1 System.out.println("取錢成功, 取出錢數爲:" + drawAmount); double balance = account.getBalance() - drawAmount; account.setBalance(balance); System.out.println("餘額爲:" + balance); } } } class Account { private String accountNo; private double balance; public Account() { } public Account(String accountNo, double balance) { this.accountNo = accountNo; this.balance = balance; } public String getAccountNo() { return accountNo; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } }
上面例子很容易理解,有一張銀行卡,裏面有1000的餘額,程序模擬你和你老婆同時在取款機進行取錢操做的場景。屢次運行此程序,可能具備多個不一樣組合的輸出結果。其中一種可能的輸出爲:編程
1 取錢成功, 取出錢數爲:700.0 2 餘額爲:300.0 3 取錢成功, 取出錢數爲:700.0 4 餘額爲:-400.0
也就是說,對於一張只有1000餘額的銀行卡,大家一共能夠取出1400,這顯然是有問題的。安全
通過分析,問題在於Java多線程環境下的執行的不肯定性。CPU可能隨機的在多個處於就緒狀態中的線程中進行切換,所以,頗有可能出現以下狀況:當thread1執行到//1處代碼時,判斷條件爲true,此時CPU切換到thread2,執行//1處代碼,發現依然爲真,而後執行完thread2,接着切換到thread1,接着執行完畢。此時,就會出現上述結果。多線程
所以,講到線程安全問題,實際上是指多線程環境下對共享資源的訪問可能會引發此共享資源的不一致性。所以,爲避免線程安全問題,應該避免多線程環境下對此共享資源的併發訪問。併發
二.同步方法ide
對共享資源進行訪問的方法定義中加上synchronized關鍵字修飾,使得此方法稱爲同步方法。能夠簡單理解成對此方法進行了加鎖,其鎖對象爲當前方法所在的對象自身。多線程環境下,當執行此方法時,首先都要得到此同步鎖(且同時最多隻有一個線程可以得到),只有當線程執行完此同步方法後,纔會釋放鎖對象,其餘的線程纔有可能獲取此同步鎖,以此類推...測試
在上例中,共享資源爲account對象,當使用同步方法時,能夠解決線程安全問題。只需在run()方法前加上synshronized關鍵字便可。this
public synchronized void run() { // .... }
三.同步代碼塊spa
正如上面所分析的那樣,解決線程安全問題其實只需限制對共享資源訪問的不肯定性便可。使用同步方法時,使得整個方法體都成爲了同步執行狀態,會使得可能出現同步範圍過大的狀況,因而,針對須要同步的代碼能夠直接另外一種同步方式——同步代碼塊來解決。
同步代碼塊的格式爲:
synchronized (obj) { //... }
其中,obj爲鎖對象,所以,選擇哪個對象做爲鎖是相當重要的。通常狀況下,都是選擇此共享資源對象做爲鎖對象。
如上例中,最好選用account對象做爲鎖對象。(固然,選用this也是能夠的,那是由於建立線程使用了runnable方式,若是是直接繼承Thread方式建立的線程,使用this對象做爲同步鎖會其實沒有起到任何做用,由於是不一樣的對象了。所以,選擇同步鎖時須要格外當心...)
例如:
package test.thread; import java.io.IOException; import org.junit.Test; /* * 測試線程鎖 */ public class TestBlock { public static void main(String[] args) { TestBlock test = new TestBlock(); MyTest thread1 = test.new MyTest(test); thread1.setName("1"); MyTest thread2 = test.new MyTest(test); thread2.setName("2"); thread1.start(); thread2.start(); } /* * 測試同步 */ class MyTest extends Thread { private Object o; public MyTest(Object o) { this.o = o; } @Override public void run() { // TODO Auto-generated method stub synchronized (o) { // 這個o是test對象的實例 ,對類對象實例進行加鎖,當線程調用一個實例運行的,另外的線程調用這個實例時候阻塞,達到上鎖的目的 try { for (int a = 0; a < 10; a++) { System.out.println("線程" + MyTest.currentThread().getName() + "修改a==" + a); // MyTest.yield(); } } catch (Exception e) { // TODO: handle exception } } } } } 返回的結果: · 線程1修改a==0 線程1修改a==1 線程1修改a==2 線程1修改a==3 線程1修改a==4 線程1修改a==5 線程1修改a==6 線程1修改a==7 線程1修改a==8 線程1修改a==9 線程2修改a==0 線程2修改a==1 線程2修改a==2 線程2修改a==3 線程2修改a==4 線程2修改a==5 線程2修改a==6 線程2修改a==7 線程2修改a==8 線程2修改a==9 能夠看到當一個線程運行完畢以後才運行第二個線程
package myTest; /* * 測試線程鎖 */ public class TestBlock { // 調用類 public static void main(String[] args) { TestBlock test = new TestBlock(); MyTest thread1 = test.new MyTest(test); thread1.setName("1"); MyTest thread2 = test.new MyTest(test); thread2.setName("2"); thread1.start(); thread2.start(); } /* * 測試同步 */ class MyTest extends Thread { private Object o; public MyTest(Object o) { this.o = o; } @Override public void run() { synchronized (this) { // this 指代當時類 也就是MyTest,兩個線程同時調用同一個類方法。就是兩個線程對兩個實例的各自上鎖。互相不阻塞 try { for (int a = 0; a < 10; a++) { System.out.println("線程" + MyTest.currentThread().getName() + "修改a==" + a); // MyTest.yield(); } } catch (Exception e) { // TODO: handle exception } } } } } 返回的結果: 線程1修改a==0 線程1修改a==1 線程2修改a==0 線程2修改a==1 線程2修改a==2 線程2修改a==3 線程1修改a==2 線程1修改a==3 線程1修改a==4 線程1修改a==5 線程1修改a==6 線程1修改a==7 線程1修改a==8 線程1修改a==9 線程2修改a==4 線程2修改a==5 線程2修改a==6 線程2修改a==7 線程2修改a==8 線程2修改a==9 能夠看到:兩個線程互不阻塞,鎖住的this是不一樣的對象
利用實現了Runnable接口的類建立線程,也是同樣
public class ThreadTest { public static void main(String[] args) { ThreadTest test = new ThreadTest(); //這裏只能new 一個Runnable接口的實現類,若是兩個線程使用不一樣的Runnable做爲入參,那麼同步代碼塊中鎖住的this會是不一樣的對象 Runnable runnable = test.new MyTest(test); // Runnable runnable2 = test.new MyTest(test); Thread thread1 = new Thread(runnable); // 將myRunnable做爲Thread target建立新的線程 thread1.setName("1"); Thread thread2 = new Thread(runnable); // Thread thread2 = new Thread(runnable2); thread2.setName("2"); /*若是直接調用run()方法,調用的是覆蓋了Runnable接口的run方法,此時並無啓動線程,能夠發現打印的結果爲: * 線程main修改a==0 線程main修改a==1 線程main修改a==2 線程main修改a==0 線程main修改a==1 線程main修改a==2 thread1.run(); thread2.run(); */ /*調用start()方法才能正確的啓動線程,而且鎖住的this是同一個對象,同步鎖有效,輸出結果爲: * 線程1修改a==0 線程1修改a==1 線程1修改a==2 線程2修改a==0 線程2修改a==1 線程2修改a==2 */ thread1.start(); thread2.start(); } /* * 測試同步 */ class MyTest implements Runnable{ private Object o; public MyTest(Object o){ this.o=o; } @Override public void run() { // TODO Auto-generated method stub synchronized (this) { //這個o是test對象的實例 ,對類對象實例進行加鎖,當線程調用一個實例運行的,另外的線程調用這個實例時候阻塞,達到上鎖的目的 try { for(int a=0;a<3;a++){ System.out.println("線程"+Thread.currentThread().getName()+"修改a=="+a); //MyTest.yield(); } } catch (Exception e) { // TODO: handle exception } } } } }
輸出結果:
線程1修改a==0 線程1修改a==1 線程1修改a==2 線程2修改a==0 線程2修改a==1 線程2修改a==2
四.Lock對象同步鎖
上面咱們能夠看出,正由於對同步鎖對象的選擇須要如此當心,有沒有什麼簡單點的解決方案呢?以方便同步鎖對象與共享資源解耦,同時又能很好的解決線程安全問題。當使用Lock來保證線程同步時,需使用Condition對象來使線程保持協調。Condition實例被綁定在一個Lock的對象上,使用Lock對象的方法newCondition()獲取Condition的實例。Condition提供了下面三種方法,來協調不一樣線程的同步:
一、await():致使當前線程等待,直到其餘線程調用該Condition的signal()或signalAll()方法喚醒該線程。
二、signal():喚醒在此Lock對象上等待的單個線程。
三、signalAll():喚醒在此Lock對象上等待的全部線程。
使用Lock對象同步鎖能夠方便的解決此問題,惟一須要注意的一點是Lock對象須要與資源對象一樣具備一對一的關係。Lock對象同步鎖通常格式爲:
class X { // 顯示定義Lock同步鎖對象,此對象與共享資源具備一對一關係 private final Lock lock = new ReentrantLock(); public void m(){ // 加鎖 lock.lock(); //... 須要進行線程安全同步的代碼 // 釋放Lock鎖 lock.unlock(); } }
五.wait()/notify()/notifyAll()線程通訊
在博文《Java總結篇系列:java.lang.Object》中有說起到這三個方法,雖然這三個方法主要都是用於多線程中,但實際上都是Object類中的本地方法。所以,理論上,任何Object對象均可以做爲這三個方法的主調,在實際的多線程編程中,只有同步鎖對象調這三個方法,才能完成對多線程間的線程通訊。
wait():致使當前線程等待並使其進入到等待阻塞狀態。直到其餘線程調用該同步鎖對象的notify()或notifyAll()方法來喚醒此線程。
notify():喚醒在此同步鎖對象上等待的單個線程,若是有多個線程都在此同步鎖對象上等待,則會任意選擇其中某個線程進行喚醒操做,只有當前線程放棄對同步鎖對象的鎖定,纔可能執行被喚醒的線程。
notifyAll():喚醒在此同步鎖對象上等待的全部線程,只有當前線程放棄對同步鎖對象的鎖定,纔可能執行被喚醒的線程。
package myTest; public class ThreadTest { public static void main(String[] args) { Account account = new Account("123456", 0); Thread drawMoneyThread = new DrawMoneyThread("取錢線程", account, 700); Thread depositeMoneyThread = new DepositeMoneyThread("存錢線程", account, 700); drawMoneyThread.start(); depositeMoneyThread.start(); } } class DrawMoneyThread extends Thread { private Account account; private double amount; public DrawMoneyThread(String threadName, Account account, double amount) { super(threadName); this.account = account; this.amount = amount; } public void run() { for (int i = 0; i < 100; i++) { account.draw(amount, i); } } } class DepositeMoneyThread extends Thread { private Account account; private double amount; public DepositeMoneyThread(String threadName, Account account, double amount) { super(threadName); this.account = account; this.amount = amount; } public void run() { for (int i = 0; i < 100; i++) { account.deposite(amount, i); } } } class Account { private String accountNo; private double balance; // 標識帳戶中是否已有存款 private boolean flag = false; public Account() { } public Account(String accountNo, double balance) { this.accountNo = accountNo; this.balance = balance; } public String getAccountNo() { return accountNo; } public void setAccountNo(String accountNo) { this.accountNo = accountNo; } public double getBalance() { return balance; } public void setBalance(double balance) { this.balance = balance; } /** * 存錢 * * @param depositeAmount */ public synchronized void deposite(double depositeAmount, int i) { if (flag) { // 帳戶中已有人存錢進去,此時當前線程須要等待阻塞 try { System.out.println(Thread.currentThread().getName() + " 開始要執行wait操做" + " -- i=" + i); wait(); // 1 System.out.println(Thread.currentThread().getName() + " 執行了wait操做" + " -- i=" + i); } catch (InterruptedException e) { e.printStackTrace(); } } else { // 開始存錢 System.out.println(Thread.currentThread().getName() + " 存款:" + depositeAmount + " -- i=" + i); setBalance(balance + depositeAmount); flag = true; // 喚醒其餘線程 notifyAll(); // 2 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "-- 存錢 -- 執行完畢" + " -- i=" + i); } } /** * 取錢 * * @param drawAmount */ public synchronized void draw(double drawAmount, int i) { if (!flag) { // 帳戶中還沒人存錢進去,此時當前線程須要等待阻塞 try { System.out.println(Thread.currentThread().getName() + " 開始要執行wait操做" + " 執行了wait操做" + " -- i=" + i); wait(); System.out.println(Thread.currentThread().getName() + " 執行了wait操做" + " 執行了wait操做" + " -- i=" + i); } catch (InterruptedException e) { e.printStackTrace(); } } else { // 開始取錢 System.out.println(Thread.currentThread().getName() + " 取錢:" + drawAmount + " -- i=" + i); setBalance(getBalance() - drawAmount); flag = false; // 喚醒其餘線程 notifyAll(); System.out.println(Thread.currentThread().getName() + "-- 取錢 -- 執行完畢" + " -- i=" + i); // 3 } } }
上面的例子演示了wait()/notify()/notifyAll()的用法。部分輸出結果爲:
1 取錢線程 開始要執行wait操做 執行了wait操做 -- i=0 2 存錢線程 存款:700.0 -- i=0 3 存錢線程-- 存錢 -- 執行完畢 -- i=0 4 存錢線程 開始要執行wait操做 -- i=1 5 取錢線程 執行了wait操做 執行了wait操做 -- i=0 6 取錢線程 取錢:700.0 -- i=1 7 取錢線程-- 取錢 -- 執行完畢 -- i=1 8 取錢線程 開始要執行wait操做 執行了wait操做 -- i=2 9 存錢線程 執行了wait操做 -- i=1 10 存錢線程 存款:700.0 -- i=2 11 存錢線程-- 存錢 -- 執行完畢 -- i=2 12 取錢線程 執行了wait操做 執行了wait操做 -- i=2 13 取錢線程 取錢:700.0 -- i=3 14 取錢線程-- 取錢 -- 執行完畢 -- i=3 15 取錢線程 開始要執行wait操做 執行了wait操做 -- i=4 16 存錢線程 存款:700.0 -- i=3 17 存錢線程-- 存錢 -- 執行完畢 -- i=3 18 存錢線程 開始要執行wait操做 -- i=4 19 取錢線程 執行了wait操做 執行了wait操做 -- i=4 20 取錢線程 取錢:700.0 -- i=5 21 取錢線程-- 取錢 -- 執行完畢 -- i=5 22 取錢線程 開始要執行wait操做 執行了wait操做 -- i=6 23 存錢線程 執行了wait操做 -- i=4 24 存錢線程 存款:700.0 -- i=5 25 存錢線程-- 存錢 -- 執行完畢 -- i=5 26 存錢線程 開始要執行wait操做 -- i=6 27 取錢線程 執行了wait操做 執行了wait操做 -- i=6 28 取錢線程 取錢:700.0 -- i=7 29 取錢線程-- 取錢 -- 執行完畢 -- i=7 30 取錢線程 開始要執行wait操做 執行了wait操做 -- i=8 31 存錢線程 執行了wait操做 -- i=6 32 存錢線程 存款:700.0 -- i=7
由此,咱們須要注意以下幾點:
1.wait()方法執行後,當前線程當即進入到等待阻塞狀態,wait()方法會當即使當前線程暫停執行並釋放對象鎖標示.其後面的代碼將不會執行;運行的線程執行wait()方法,JVM會把該線程放入等待池中(wait會釋放持有的鎖)。wait是指在一個已經進入了同步鎖的線程內,讓本身暫時讓出同步鎖,以便其餘正在等待此鎖的線程能夠獲得同步鎖並運行,只有其餘線程調用了notify方法(notify並不釋放鎖,只是告訴調用過wait方法的線程能夠去參與得到鎖的競爭了,但不是立刻獲得鎖,由於鎖還在別人手裏,別人還沒釋放,若是notify()/notifyAll()後面還有代碼,還會繼續進行,直到當前線程執行完畢纔會釋放同步鎖對象,即當前線程原來要怎麼執行,調用了nodity()以後還的怎麼執行,對它自己的代碼的執行沒有影響,並不會釋放鎖),調用wait方法的一個或多個線程就會解除wait狀態,從新參與競爭對象鎖,線程若是再次獲得鎖,才能夠繼續向下運行。當前線程必須擁有此對象的monitor(即鎖),才能調用某個對象的wait()方法能讓當前線程阻塞,(這種阻塞是經過提早釋放synchronized鎖,從新去請求鎖致使的阻塞,這種請求必須有其餘線程經過notify()或者notifyAll()喚醒從新競爭得到鎖)
2.notify()/notifyAll()方法執行後,將喚醒此同步鎖對象上的(任意一個-notify()/全部-notifyAll())線程對象,可是,此時還並無釋放同步鎖對象,也就是說,若是notify()/notifyAll()後面還有代碼,還會繼續進行,直到當前線程執行完畢纔會釋放同步鎖對象;當調用notify()方法後,將從對象的等待池中移走一個任意的線程並放到鎖標誌等待池中,只有鎖標誌等待池中線程可以獲取鎖標誌;若是鎖標誌等待池中沒有線程,則notify()不起做用。notifyAll()則從對象等待池中移走全部等待那個對象的線程並放到鎖標誌等待池中。調用某個對象的notify()方法可以喚醒一個正在等待這個對象的monitor(即鎖)的線程,若是有多個線程都在等待這個對象的monitor,則只能喚醒其中一個線程; notify()或者notifyAll()方法並非真正釋放鎖,必須等到synchronized方法或者語法塊執行完才真正釋放鎖)。調用notifyAll()方法可以喚醒全部正在等待這個對象的monitor的線程,喚醒的線程得到鎖的機率是隨機的,取決於cpu調度。
3.notify()/notifyAll()執行後,若是後面有sleep()方法,則會使當前線程進入到阻塞狀態,可是sleep()方法並不會釋放同步鎖,同步鎖依然本身保留,那麼必定在sleep的時間後會繼續執行此線程,接下來同2;
4.wait()/notify()/nitifyAll()完成線程間的通訊或協做都是基於不一樣對象鎖的,所以,若是是不一樣的同步對象鎖將失去意義,同時,同步對象鎖最好是與共享資源對象保持一一對應關係;這三個方法都是java.lang.Object的方法。
5.當wait線程喚醒後並執行時,是接着上次執行到的wait()方法代碼後面繼續往下執行的。
6.Thread.yield()方法做用是:yield()方法是中止當前線程,讓同等優先權的線程運行,並執行其餘線程,如果沒有同等優先權的線程,那麼yield()方法將不會起做用。yield()應該作的是讓當前運行線程回到可運行狀態,以容許具備相同優先級的其餘線程得到運行機會。所以,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。可是,實際中沒法保證yield()必定會達到讓步的目的,由於讓步的線程還有可能被線程調度程序再次選中。yield()會使當前線程從執行狀態(運行狀態)變爲可執行態(就緒狀態),此時cpu會從衆多的可執行狀態的線程裏中選擇,也就是說,當前也就是剛剛的那個線程仍是有可能會被再次執行到的,並非說必定會執行其餘線程而該線程在下一次中不會執行到了。使用了yield方法後,該線程就會把CPU時間讓掉,讓其餘或者本身的線程執行(也就是誰先搶到誰執行)。
結論:yield()從未致使線程轉到等待/睡眠/阻塞狀態。在大多數狀況下,yield()將致使線程從運行狀態轉到可運行狀態,但有可能沒有效果。yield()暫停當前正在執行的線程對象,並執行其餘線程。
yield和sleep的區別:
① sleep()方法給其餘線程運行機會時不考慮線程的優先級,所以會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會;
② 線程執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態;
③ sleep()方法聲明拋出InterruptedException,而yield()方法沒有聲明任何異常;
④ sleep()方法比yield()方法(跟操做系統CPU調度相關)具備更好的可移植性。sleep(0)和yield()我以爲效果差很少,都是從新進行cpu的從新調度,此時cpu會從衆多的可執行狀態的線程裏中選擇
7.join()方法的做用:join()方法可使得一個線程在另外一個線程結束後再執行。若是join()方法在一個線程實例上調用,那麼當前運行着的線程將阻塞直到調用了join()方法的線程實例完成了以後才能繼續執行。主線程等待 調用了join()方法的線程完成以後纔會被喚醒,以前一直是 wait 狀態(即在子線程線程執行完以前,主線程一直是wait狀態,直到子線程執行完,才被喚醒,繼續執行主線程)
join()方法的底層是利用wait()方法實現的。能夠看出,join方法是一個synchronized 包裹的同步方法,假設主線程調用線程threadA.join()方法時,由於join()被synchronized 修飾,因此主線程先得到了threadA的鎖,隨後進入join方法,在join方法中調用了threadA的wait()方法,使主線程進入了threadA對象的等待池,此時,threadA線程則還在執行,只有等到threadA線程執行完畢以後,纔會釋放threadA鎖,此時主線程被喚醒,主線程得以繼續執行,threadA的join()方法只會影響到主線程和threadA之間的執行順序,並不影響同一時刻處在運行狀態的其餘線程。其餘線程原來是怎麼運行的,如今仍是怎麼運行,絲毫沒有收到threadA.join()方法的影響。
8.執行sleep()的線程在指定的時間內確定不會被執行;sleep()方法只讓出了CPU,而並不會釋放同步資源鎖。sleep()方法使當前運行中的線程睡眼一段時間,進入不可運行狀態,這段時間的長短是由程序設定的,而yield()方法會使當前線程讓出 CPU 佔有權,只是讓出的時間是不可設定的。實際上,yield()方法對應了以下操做:先檢測當前是否有相同優先級的線程處於同可運行狀態,若有,則把 CPU 的佔有權交給此線程,不然,繼續運行原來的線程。因此yield()方法稱爲「退讓」,它把運行機會讓給了同等優先級的其餘線程,可是該線程自己仍是會可能被再次調度。 另外,sleep()方法容許較低優先級的線程得到運行機會,但 yield() 方法執行時,當前線程仍處在可運行狀態,因此,不可能讓出較低優先級的線程些時得到 CPU 佔有權。
問:Java多線程運行環境中,在哪些狀況下會使對象鎖釋放? 答:因爲等待一個鎖的線程只有在得到這把鎖以後,才能恢復運行,因此讓持有鎖的線程在再也不須要鎖的時候及時釋放鎖是很重要的。在如下狀況下,持有鎖的線程會釋放鎖: (1)執行完同步代碼塊,就會釋放鎖。(synchronized) (2)在執行同步代碼塊的過程當中,遇到異常而致使線程終止,鎖也會被釋放。(exception) (3)在執行同步代碼塊的過程當中,執行了鎖所屬對象的wait()方法,這個線程會釋放鎖,進 入對象的等待池。(wait) 除了以上狀況之外,只要持有鎖的線程尚未執行完同步代碼塊,就不會釋放鎖。 在下面狀況下,線程是不會釋放鎖的: (1)執行同步代碼塊的過程當中,執行了Thread.sleep()方法,當前線程放棄CPU,開始睡眠,在睡眠中不會釋放鎖。 (2)在執行同步代碼塊的過程當中,執行了Thread.yield()方法,當前線程放棄CPU,此時當前線程會加入搶奪鎖的行列,當前線程有也可能會從新得到鎖,獲得執行的機會,但不會釋放鎖。 (3)在執行同步代碼塊的過程當中,其餘線程執行了當前線程對象的suspend()方法,當前線程被暫停,但不會釋放鎖。