Java—多線程編程

一個多線程程序包含兩個或多個能併發運行的部分。程序的每一部分都稱做一個線程,而且每一個線程定義了一個獨立的執行路徑。javascript

進程:一個進程包括由操做系統分配的內存空間,包含一個或多個線程。一個線程不能獨立的存在,它必須是進程的一部分。一個進程一直運行,直到全部的非守候線程都結束運行後才能結束。html

多線程能知足程序員編寫高效率的程序來達到充分利用CPU的目的。java

一 生命週期程序員

  • 新建狀態:

    使用 new 關鍵字和 Thread 類或其子類創建一個線程對象後,該線程對象就處於新建狀態。它保持這個狀態直到程序 start() 這個線程。編程

  • 就緒狀態:

    當線程對象調用了start()方法以後,該線程就進入就緒狀態。就緒狀態的線程處於就緒隊列中,要等待JVM裏線程調度器的調度。安全

  • 運行狀態:

    若是就緒狀態的線程獲取 CPU 資源,就能夠執行 run(),此時線程便處於運行狀態。處於運行狀態的線程最爲複雜,它能夠變爲阻塞狀態、就緒狀態和死亡狀態。多線程

  • 阻塞狀態:

    若是一個線程執行了sleep(睡眠)、suspend(掛起)等方法,失去所佔用資源以後,該線程就從運行狀態進入阻塞狀態。在睡眠時間已到或得到設備資源後能夠從新進入就緒狀態。併發

  • 死亡狀態:

    一個運行狀態的線程完成任務或者其餘終止條件發生時,該線程就切換到終止狀態。ide

二 線程的建立與啓動post

Java中線程的建立常見有如三種基本形式

1.繼承Thread類,重寫該類的run()方法。

建立一個新的類,該類繼承Thread類,而後建立一個該類的實例。

 

繼承類必須重寫run()方法,該方法是新線程的入口點。它也必須調用start()方法才能執行。

 

複製代碼
 1 class MyThread extends Thread {  2  3 private int i = 0;  4  5  @Override  6 public void run() {  7 for (i = 0; i < 100; i++) {  8 System.out.println(Thread.currentThread().getName() + " " + i);  9  } 10  } 11 }
複製代碼
複製代碼
 1 public class ThreadTest {  2  3 public static void main(String[] args) {  4 for (int i = 0; i < 100; i++) {  5 System.out.println(Thread.currentThread().getName() + " " + i);  6 if (i == 30) {  7 Thread myThread1 = new MyThread(); // 建立一個新的線程 myThread1 此線程進入新建狀態  8 Thread myThread2 = new MyThread(); // 建立一個新的線程 myThread2 此線程進入新建狀態  9 myThread1.start(); // 調用start()方法使得線程進入就緒狀態 10 myThread2.start(); // 調用start()方法使得線程進入就緒狀態 11  } 12  } 13  } 14 }
複製代碼

輸出結果:(多線程執行)存在交叉性。

如上所示,繼承Thread類,經過重寫run()方法定義了一個新的線程類MyThread,其中run()方法的方法體表明瞭線程須要完成的任務,稱之爲線程執行體。當建立此線程類對象時一個新的線程得以建立,並進入到線程新建狀態。經過調用線程對象引用的start()方法,使得該線程進入到就緒狀態,此時此線程並不必定會立刻得以執行,這取決於CPU調度時機。

2.實現Runnable接口,並重寫該接口的run()方法,該run()方法一樣是線程執行體,建立Runnable實現類的實例,並以此實例做爲Thread類的target來建立Thread對象,該Thread對象纔是真正的線程對象。

 

建立一個線程,最簡單的方法是建立一個實現Runnable接口的類。爲了實現Runnable,一個類只須要執行一個方法調用run(),

 

複製代碼
 1 class MyRunnable implements Runnable {  2 private int i = 0;  3  4  @Override  5 public void run() {  6 for (i = 0; i < 100; i++) {  7 System.out.println(Thread.currentThread().getName() + " " + i);  8  }  9  } 10 }
複製代碼
複製代碼
 1 public class ThreadTest {  2  3 public static void main(String[] args) {  4 for (int i = 0; i < 100; i++) {  5 System.out.println(Thread.currentThread().getName() + " " + i);  6 if (i == 30) {  7 Runnable myRunnable = new MyRunnable(); // 建立一個Runnable實現類的對象  8 Thread thread1 = new Thread(myRunnable); // 將myRunnable做爲Thread target建立新的線程  9 Thread thread2 = new Thread(myRunnable); 10 thread1.start(); // 調用start()方法使得線程進入就緒狀態 11  thread2.start(); 12  } 13  } 14  } 15 }
複製代碼

相信以上兩種建立新線程的方式你們都很熟悉了,那麼Thread和Runnable之間究竟是什麼關係呢?咱們首先來看一下下面這個例子。

複製代碼
 1 public class ThreadTest {  2  3 public static void main(String[] args) {  4 for (int i = 0; i < 100; i++) {  5 System.out.println(Thread.currentThread().getName() + " " + i);  6 if (i == 30) {  7 Runnable myRunnable = new MyRunnable();  8 Thread thread = new MyThread(myRunnable);  9  thread.start(); 10  } 11  } 12  } 13 } 14 15 class MyRunnable implements Runnable { 16 private int i = 0; 17 18  @Override 19 public void run() { 20 System.out.println("in MyRunnable run"); 21 for (i = 0; i < 100; i++) { 22 System.out.println(Thread.currentThread().getName() + " " + i); 23  } 24  } 25 } 26 27 class MyThread extends Thread { 28 29 private int i = 0; 30 31 public MyThread(Runnable runnable){ 32 super(runnable); 33  } 34 35  @Override 36 public void run() { 37 System.out.println("in MyThread run"); 38 for (i = 0; i < 100; i++) { 39 System.out.println(Thread.currentThread().getName() + " " + i); 40  } 41  } 42 }
複製代碼

一樣的,與實現Runnable接口建立線程方式類似,不一樣的地方在於

1 Thread thread = new MyThread(myRunnable);

那麼這種方式能夠順利建立出一個新的線程麼?答案是確定的。至於此時的線程執行體究竟是MyRunnable接口中的run()方法仍是MyThread類中的run()方法呢?經過輸出咱們知道線程執行體是MyThread類中的run()方法。其實緣由很簡單,由於Thread類自己也是實現了Runnable接口,而run()方法最早是在Runnable接口中定義的方法。

1 public interface Runnable { 2 3 public abstract void run(); 4 5 }

咱們看一下Thread類中對Runnable接口中run()方法的實現:

複製代碼
  @Override public void run() { if (target != null) { target.run(); } }
複製代碼

也就是說,當執行到Thread類中的run()方法時,會首先判斷target是否存在,存在則執行target中的run()方法,也就是實現了Runnable接口並重寫了run()方法的類中的run()方法。可是上述給到的列子中,因爲多態的存在,根本就沒有執行到Thread類中的run()方法,而是直接先執行了運行時類型即MyThread類中的run()方法。

3.使用Callable和Future接口建立線程。具體是建立Callable接口的實現類,並實現clall()方法。並使用FutureTask類來包裝Callable實現類的對象,且以此FutureTask對象做爲Thread對象的target來建立線程。

 看着好像有點複雜,直接來看一個例子就清晰了。

複製代碼
 1 public class ThreadTest {  2  3 public static void main(String[] args) {  4  5 Callable<Integer> myCallable = new MyCallable(); // 建立MyCallable對象  6 FutureTask<Integer> ft = new FutureTask<Integer>(myCallable); //使用FutureTask來包裝MyCallable對象  7  8 for (int i = 0; i < 100; i++) {  9 System.out.println(Thread.currentThread().getName() + " " + i); 10 if (i == 30) { 11 Thread thread = new Thread(ft); //FutureTask對象做爲Thread對象的target建立新的線程 12 thread.start(); //線程進入到就緒狀態 13  } 14  } 15 16 System.out.println("主線程for循環執行完畢.."); 17 18 try { 19 int sum = ft.get(); //取得新建立的新線程中的call()方法返回的結果 20 System.out.println("sum = " + sum); 21 } catch (InterruptedException e) { 22  e.printStackTrace(); 23 } catch (ExecutionException e) { 24  e.printStackTrace(); 25  } 26 27  } 28 } 29 30 31 class MyCallable implements Callable<Integer> { 32 private int i = 0; 33 34 // 與run()方法不一樣的是,call()方法具備返回值 35  @Override 36 public Integer call() { 37 int sum = 0; 38 for (; i < 100; i++) { 39 System.out.println(Thread.currentThread().getName() + " " + i); 40 sum += i; 41  } 42 return sum; 43  } 44 45 }
複製代碼

首先,咱們發現,在實現Callable接口中,此時再也不是run()方法了,而是call()方法,此call()方法做爲線程執行體,同時還具備返回值!在建立新的線程時,是經過FutureTask來包裝MyCallable對象,同時做爲了Thread對象的target。那麼看下FutureTask類的定義:

1 public class FutureTask<V> implements RunnableFuture<V> { 2 3 //.... 4 5 }
1 public interface RunnableFuture<V> extends Runnable, Future<V> { 2 3 void run(); 4 5 }

因而,咱們發現FutureTask類其實是同時實現了Runnable和Future接口,由此才使得其具備Future和Runnable雙重特性。經過Runnable特性,能夠做爲Thread對象的target,而Future特性,使得其能夠取得新建立線程中的call()方法的返回值。

執行下此程序,咱們發現sum = 4950永遠都是最後輸出的。而「主線程for循環執行完畢..」則極可能是在子線程循環中間輸出。由CPU的線程調度機制,咱們知道,「主線程for循環執行完畢..」的輸出時機是沒有任何問題的,那麼爲何sum =4950會永遠最後輸出呢?

緣由在於經過ft.get()方法獲取子線程call()方法的返回值時,當子線程此方法還未執行完畢,ft.get()方法會一直阻塞,直到call()方法執行完畢才能取到返回值。

上述主要講解了三種常見的線程建立方式,對於線程的啓動而言,都是調用線程對象的start()方法,須要特別注意的是:不能對同一線程對象兩次調用start()方法。

 

三. Java多線程的就緒、運行和死亡狀態

就緒狀態轉換爲運行狀態:當此線程獲得處理器資源;

運行狀態轉換爲就緒狀態:當此線程主動調用yield()方法或在運行過程當中失去處理器資源。

運行狀態轉換爲死亡狀態:當此線程線程執行體執行完畢或發生了異常。

此處須要特別注意的是:當調用線程的yield()方法時,線程從運行狀態轉換爲就緒狀態,但接下來CPU調度就緒狀態中的哪一個線程具備必定的隨機性,所以,可能會出現A線程調用了yield()方法後,接下來CPU仍然調度了A線程的狀況。

因爲實際的業務須要,經常會遇到須要在特定時機終止某一線程的運行,使其進入到死亡狀態。目前最通用的作法是設置一boolean型的變量,當條件知足時,使線程執行體快速執行完畢。如:

複製代碼
 1 public class ThreadTest {  2  3 public static void main(String[] args) {  4  5 MyRunnable myRunnable = new MyRunnable();  6 Thread thread = new Thread(myRunnable);  7  8 for (int i = 0; i < 100; i++) {  9 System.out.println(Thread.currentThread().getName() + " " + i); 10 if (i == 30) { 11  thread.start(); 12  } 13 if(i == 40){ 14  myRunnable.stopThread(); 15  } 16  } 17  } 18 } 19 20 class MyRunnable implements Runnable { 21 22 private boolean stop; 23 24  @Override 25 public void run() { 26 for (int i = 0; i < 100 && !stop; i++) { 27 System.out.println(Thread.currentThread().getName() + " " + i); 28  } 29  } 30 31 public void stopThread() { 32 this.stop = true; 33  } 34 35 }
複製代碼

 

四.Java多線程的阻塞狀態與線程控制

上文已經提到Java阻塞的幾種具體類型。下面分別看下引發Java線程阻塞的主要方法。

1.join()

join —— 讓一個線程等待另外一個線程完成才繼續執行。如A線程線程執行體中調用B線程的join()方法,則A線程被阻塞,知道B線程執行完爲止,A才能得以繼續執行。

複製代碼
 1 public class ThreadTest {  2  3 public static void main(String[] args) {  4  5 MyRunnable myRunnable = new MyRunnable();  6 Thread thread = new Thread(myRunnable);  7  8 for (int i = 0; i < 100; i++) {  9 System.out.println(Thread.currentThread().getName() + " " + i); 10 if (i == 30) { 11  thread.start(); 12 try { 13 thread.join(); // main線程須要等待thread線程執行完後才能繼續執行 14 } catch (InterruptedException e) { 15  e.printStackTrace(); 16  } 17  } 18  } 19  } 20 } 21 22 class MyRunnable implements Runnable { 23 24  @Override 25 public void run() { 26 for (int i = 0; i < 100; i++) { 27 System.out.println(Thread.currentThread().getName() + " " + i); 28  } 29  } 30 }
複製代碼

 

2.sleep()

sleep —— 讓當前的正在執行的線程暫停指定的時間,並進入阻塞狀態。在其睡眠的時間段內,該線程因爲不是處於就緒狀態,所以不會獲得執行的機會。即便此時系統中沒有任何其餘可執行的線程,出於sleep()中的線程也不會執行。所以sleep()方法經常使用來暫停線程執行。

前面有講到,當調用了新建的線程的start()方法後,線程進入到就緒狀態,可能會在接下來的某個時間獲取CPU時間片得以執行,若是但願這個新線程必然性的當即執行,直接調用原來線程的sleep(1)便可。

複製代碼
 1 public class ThreadTest {  2  3 public static void main(String[] args) {  4  5 MyRunnable myRunnable = new MyRunnable();  6 Thread thread = new Thread(myRunnable);  7  8 for (int i = 0; i < 100; i++) {  9 System.out.println(Thread.currentThread().getName() + " " + i); 10 if (i == 30) { 11  thread.start(); 12 try { 13 Thread.sleep(1); // 使得thread必然可以立刻得以執行 14 } catch (InterruptedException e) { 15  e.printStackTrace(); 16  } 17  } 18  } 19  } 20 } 21 22 class MyRunnable implements Runnable { 23 24  @Override 25 public void run() { 26 for (int i = 0; i < 100; i++) { 27 System.out.println(Thread.currentThread().getName() + " " + i); 28  } 29  } 30 }
複製代碼

注:睡一個毫秒級夠了,由於CPU不會空閒,會切換到新建的線程。

 

3.後臺線程(Daemon Thread)

概念/目的:後臺線程主要是爲其餘線程(相對能夠稱之爲前臺線程)提供服務,或「守護線程」。如JVM中的垃圾回收線程。

生命週期:後臺線程的生命週期與前臺線程生命週期有必定關聯。主要體如今:當全部的前臺線程都進入死亡狀態時,後臺線程會自動死亡(其實這個也很好理解,由於後臺線程存在的目的在於爲前臺線程服務的,既然全部的前臺線程都死亡了,那它本身還留着有什麼用...偉大啊 ! !)。

設置後臺線程:調用Thread對象的setDaemon(true)方法能夠將指定的線程設置爲後臺線程。

複製代碼
 1 public class ThreadTest {  2  3 public static void main(String[] args) {  4 Thread myThread = new MyThread();  5 for (int i = 0; i < 100; i++) {  6 System.out.println("main thread i = " + i);  7 if (i == 20) {  8 myThread.setDaemon(true);  9  myThread.start(); 10  } 11  } 12  } 13 14 } 15 16 class MyThread extends Thread { 17 18 public void run() { 19 for (int i = 0; i < 100; i++) { 20 System.out.println("i = " + i); 21 try { 22 Thread.sleep(1); 23 } catch (InterruptedException e) { 24 // TODO Auto-generated catch block 25  e.printStackTrace(); 26  } 27  } 28  } 29 }
複製代碼

判斷線程是不是後臺線程:調用thread對象的isDeamon()方法。

注:main線程默認是前臺線程,前臺線程建立中建立的子線程默認是前臺線程,後臺線程中建立的線程默認是後臺線程。調用setDeamon(true)方法將前臺線程設置爲後臺線程時,須要在start()方法調用以前。前天線程都死亡後,JVM通知後臺線程死亡,但從接收指令到做出響應,須要必定的時間。

 

4.改變線程的優先級/setPriority():

每一個線程在執行時都具備必定的優先級,優先級高的線程具備較多的執行機會。每一個線程默認的優先級都與建立它的線程的優先級相同。main線程默認具備普通優先級。

設置線程優先級:setPriority(int priorityLevel)。參數priorityLevel範圍在1-10之間,經常使用的有以下三個靜態常量值:

MAX_PRIORITY:10

MIN_PRIORITY:1

NORM_PRIORITY:5

獲取線程優先級:getPriority()。

注:具備較高線程優先級的線程對象僅表示此線程具備較多的執行機會,而非優先執行。

複製代碼
 1 public class ThreadTest {  2  3 public static void main(String[] args) {  4 Thread myThread = new MyThread();  5 for (int i = 0; i < 100; i++) {  6 System.out.println("main thread i = " + i);  7 if (i == 20) {  8  myThread.setPriority(Thread.MAX_PRIORITY);  9  myThread.start(); 10  } 11  } 12  } 13 14 } 15 16 class MyThread extends Thread { 17 18 public void run() { 19 for (int i = 0; i < 100; i++) { 20 System.out.println("i = " + i); 21  } 22  } 23 }
複製代碼

本文主要接着前面多線程的兩篇文章總結Java多線程中的線程安全問題。

一.一個典型的Java線程安全例子

複製代碼
 1 public class ThreadTest {  2  3 public static void main(String[] args) {  4 Account account = new Account("123456", 1000);  5 DrawMoneyRunnable drawMoneyRunnable = new DrawMoneyRunnable(account, 700);  6 Thread myThread1 = new Thread(drawMoneyRunnable);  7 Thread myThread2 = new Thread(drawMoneyRunnable);  8  myThread1.start();  9  myThread2.start(); 10  } 11 12 } 13 14 class DrawMoneyRunnable implements Runnable { 15 16 private Account account; 17 private double drawAmount; 18 19 public DrawMoneyRunnable(Account account, double drawAmount) { 20 super(); 21 this.account = account; 22 this.drawAmount = drawAmount; 23  } 24 25 public void run() { 26 if (account.getBalance() >= drawAmount) { //1 27 System.out.println("取錢成功, 取出錢數爲:" + drawAmount); 28 double balance = account.getBalance() - drawAmount; 29  account.setBalance(balance); 30 System.out.println("餘額爲:" + balance); 31  } 32  } 33 } 34 35 class Account { 36 37 private String accountNo; 38 private double balance; 39 40 public Account() { 41 42  } 43 44 public Account(String accountNo, double balance) { 45 this.accountNo = accountNo; 46 this.balance = balance; 47  } 48 49 public String getAccountNo() { 50 return accountNo; 51  } 52 53 public void setAccountNo(String accountNo) { 54 this.accountNo = accountNo; 55  } 56 57 public double getBalance() { 58 return balance; 59  } 60 61 public void setBalance(double balance) { 62 this.balance = balance; 63  } 64 65 }
複製代碼

上面例子很容易理解,有一張銀行卡,裏面有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,接着執行完畢。此時,就會出現上述結果。

所以,講到線程安全問題,實際上是指多線程環境下對共享資源的訪問可能會引發此共享資源的不一致性。所以,爲避免線程安全問題,應該避免多線程環境下對此共享資源的併發訪問。

 

二.同步方法

對共享資源進行訪問的方法定義中加上synchronized關鍵字修飾,使得此方法稱爲同步方法。能夠簡單理解成對此方法進行了加鎖,其鎖對象爲當前方法所在的對象自身。多線程環境下,當執行此方法時,首先都要得到此同步鎖(且同時最多隻有一個線程可以得到),只有當線程執行完此同步方法後,纔會釋放鎖對象,其餘的線程纔有可能獲取此同步鎖,以此類推...

在上例中,共享資源爲account對象,當使用同步方法時,能夠解決線程安全問題。只需在run()方法前加上synshronized關鍵字便可。

1 public synchronized void run() { 2 3 // .... 4 5 }

 

三.同步代碼塊

正如上面所分析的那樣,解決線程安全問題其實只需限制對共享資源訪問的不肯定性便可。使用同步方法時,使得整個方法體都成爲了同步執行狀態,會使得可能出現同步範圍過大的狀況,因而,針對須要同步的代碼能夠直接另外一種同步方式——同步代碼塊來解決。

同步代碼塊的格式爲:

1 synchronized (obj) { 2 3 //... 4 5 }

其中,obj爲鎖對象,所以,選擇哪個對象做爲鎖是相當重要的。通常狀況下,都是選擇此共享資源對象做爲鎖對象。

如上例中,最好選用account對象做爲鎖對象。(固然,選用this也是能夠的,那是由於建立線程使用了runnable方式,若是是直接繼承Thread方式建立的線程,使用this對象做爲同步鎖會其實沒有起到任何做用,由於是不一樣的對象了。所以,選擇同步鎖時須要格外當心...)

 

四.Lock對象同步鎖

上面咱們能夠看出,正由於對同步鎖對象的選擇須要如此當心,有沒有什麼簡單點的解決方案呢?以方便同步鎖對象與共享資源解耦,同時又能很好的解決線程安全問題。

使用Lock對象同步鎖能夠方便的解決此問題,惟一須要注意的一點是Lock對象須要與資源對象一樣具備一對一的關係。Lock對象同步鎖通常格式爲:

複製代碼
 1 class X {  2  3 // 顯示定義Lock同步鎖對象,此對象與共享資源具備一對一關係  4 private final Lock lock = new ReentrantLock();  5  6 public void m(){  7 // 加鎖  8  lock.lock();  9 10 //... 須要進行線程安全同步的代碼 11 12 // 釋放Lock鎖 13  lock.unlock(); 14  } 15 16 }
複製代碼

 

 五.wait()/notify()/notifyAll()線程通訊

在博文《Java總結篇系列:java.lang.Object》中有說起到這三個方法,雖然這三個方法主要都是用於多線程中,但實際上都是Object類中的本地方法。所以,理論上,任何Object對象均可以做爲這三個方法的主調,在實際的多線程編程中,只有同步鎖對象調這三個方法,才能完成對多線程間的線程通訊。

wait():致使當前線程等待並使其進入到等待阻塞狀態。直到其餘線程調用該同步鎖對象的notify()或notifyAll()方法來喚醒此線程。

notify():喚醒在此同步鎖對象上等待的單個線程,若是有多個線程都在此同步鎖對象上等待,則會任意選擇其中某個線程進行喚醒操做,只有當前線程放棄對同步鎖對象的鎖定,纔可能執行被喚醒的線程。

notifyAll():喚醒在此同步鎖對象上等待的全部線程,只有當前線程放棄對同步鎖對象的鎖定,纔可能執行被喚醒的線程。

複製代碼
 1 package com.qqyumidi;  2  3 public class ThreadTest {  4  5 public static void main(String[] args) {  6 Account account = new Account("123456", 0);  7  8 Thread drawMoneyThread = new DrawMoneyThread("取錢線程", account, 700);  9 Thread depositeMoneyThread = new DepositeMoneyThread("存錢線程", account, 700);  10  11  drawMoneyThread.start();  12  depositeMoneyThread.start();  13  }  14  15 }  16  17 class DrawMoneyThread extends Thread {  18  19 private Account account;  20 private double amount;  21  22 public DrawMoneyThread(String threadName, Account account, double amount) {  23 super(threadName);  24 this.account = account;  25 this.amount = amount;  26  }  27  28 public void run() {  29 for (int i = 0; i < 100; i++) {  30  account.draw(amount, i);  31  }  32  }  33 }  34  35 class DepositeMoneyThread extends Thread {  36  37 private Account account;  38 private double amount;  39  40 public DepositeMoneyThread(String threadName, Account account, double amount) {  41 super(threadName);  42 this.account = account;  43 this.amount = amount;  44  }  45  46 public void run() {  47 for (int i = 0; i < 100; i++) {  48  account.deposite(amount, i);  49  }  50  }  51 }  52  53 class Account {  54  55 private String accountNo;  56 private double balance;  57 // 標識帳戶中是否已有存款  58 private boolean flag = false;  59  60 public Account() {  61  62  }  63  64 public Account(String accountNo, double balance) {  65 this.accountNo = accountNo;  66 this.balance = balance;  67  }  68  69 public String getAccountNo() {  70 return accountNo;  71  }  72  73 public void setAccountNo(String accountNo) {  74 this.accountNo = accountNo;  75  }  76  77 public double getBalance() {  78 return balance;  79  }  80  81 public void setBalance(double balance) {  82 this.balance = balance;  83  }  84  85 /**  86  * 存錢  87  *  88  * @param depositeAmount  89 */  90 public synchronized void deposite(double depositeAmount, int i) {  91  92 if (flag) {  93 // 帳戶中已有人存錢進去,此時當前線程須要等待阻塞  94 try {  95 System.out.println(Thread.currentThread().getName() + " 開始要執行wait操做" + " -- i=" + i);  96  wait();  97 // 1  98 System.out.println(Thread.currentThread().getName() + " 執行了wait操做" + " -- i=" + i);  99 } catch (InterruptedException e) { 100  e.printStackTrace(); 101  } 102 } else { 103 // 開始存錢 104 System.out.println(Thread.currentThread().getName() + " 存款:" + depositeAmount + " -- i=" + i); 105 setBalance(balance + depositeAmount); 106 flag = true; 107 108 // 喚醒其餘線程 109  notifyAll(); 110 111 // 2 112 try { 113 Thread.sleep(3000); 114 } catch (InterruptedException e) { 115  e.printStackTrace(); 116  } 117 System.out.println(Thread.currentThread().getName() + "-- 存錢 -- 執行完畢" + " -- i=" + i); 118  } 119  } 120 121 /** 122  * 取錢 123  * 124  * @param drawAmount 125 */ 126 public synchronized void draw(double drawAmount, int i) { 127 if (!flag) { 128 // 帳戶中還沒人存錢進去,此時當前線程須要等待阻塞 129 try { 130 System.out.println(Thread.currentThread().getName() + " 開始要執行wait操做" + " 執行了wait操做" + " -- i=" + i); 131  wait(); 132 System.out.println(Thread.currentThread().getName() + " 執行了wait操做" + " 執行了wait操做" + " -- i=" + i); 133 } catch (InterruptedException e) { 134  e.printStackTrace(); 135  } 136 } else { 137 // 開始取錢 138 System.out.println(Thread.currentThread().getName() + " 取錢:" + drawAmount + " -- i=" + i); 139 setBalance(getBalance() - drawAmount); 140 141 flag = false; 142 143 // 喚醒其餘線程 144  notifyAll(); 145 146 System.out.println(Thread.currentThread().getName() + "-- 取錢 -- 執行完畢" + " -- i=" + i); // 3 147  } 148  } 149 150 }
複製代碼

上面的例子演示了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()方法執行後,當前線程當即進入到等待阻塞狀態,其後面的代碼不會執行;

2.notify()/notifyAll()方法執行後,將喚醒此同步鎖對象上的(任意一個-notify()/全部-notifyAll())線程對象,可是,此時還並無釋放同步鎖對象,也就是說,若是notify()/notifyAll()後面還有代碼,還會繼續進行,知道當前線程執行完畢纔會釋放同步鎖對象;

3.notify()/notifyAll()執行後,若是右面有sleep()方法,則會使當前線程進入到阻塞狀態,可是同步對象鎖沒有釋放,依然本身保留,那麼必定時候後仍是會繼續執行此線程,接下來同2;

4.wait()/notify()/nitifyAll()完成線程間的通訊或協做都是基於不一樣對象鎖的,所以,若是是不一樣的同步對象鎖將失去意義,同時,同步對象鎖最好是與共享資源對象保持一一對應關係;

5.當wait線程喚醒後並執行時,是接着上次執行到的wait()方法代碼後面繼續往下執行的。

固然,上面的例子相對來講比較簡單,只是爲了簡單示例wait()/notify()/noitifyAll()方法的用法,但其本質上說,已是一個簡單的生產者-消費者模式了。

5.線程讓步:yield()

上一篇博文中已經講到了yield()的基本做用,同時,yield()方法還與線程優先級有關,當某個線程調用yiled()方法從運行狀態轉換到就緒狀態後,CPU從就緒狀態線程隊列中只會選擇與該線程優先級相同或優先級更高的線程去執行。

複製代碼
 1 public class ThreadTest {  2  3 public static void main(String[] args) {  4 Thread myThread1 = new MyThread1();  5 Thread myThread2 = new MyThread2();  6  myThread1.setPriority(Thread.MAX_PRIORITY);  7  myThread2.setPriority(Thread.MIN_PRIORITY);  8 for (int i = 0; i < 100; i++) {  9 System.out.println("main thread i = " + i); 10 if (i == 20) { 11  myThread1.start(); 12  myThread2.start(); 13  Thread.yield(); 14  } 15  } 16  } 17 18 } 19 20 class MyThread1 extends Thread { 21 22 public void run() { 23 for (int i = 0; i < 100; i++) { 24 System.out.println("myThread 1 -- i = " + i); 25  } 26  } 27 } 28 29 class MyThread2 extends Thread { 30 31 public void run() { 32 for (int i = 0; i < 100; i++) { 33 System.out.println("myThread 2 -- i = " + i); 34  } 35  } 36 }
複製代碼
相關文章
相關標籤/搜索