Java 多線程執行個體詳解(三)_java

來源:互聯網
上載者:User

本文主要接著前面多線程的兩篇文章總結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,接著執行完畢。此時,就會出現上述結果。

因此,講到安全執行緒問題,其實是指多線程環境下對共用資源的訪問可能會引起此共用資源的不一致性。因此,為避免安全執行緒問題,應該避免多線程環境下對此共用資源的並發訪問。

二.同步方法

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

在上例中,共用資源為account對象,當使用同步方法時,可以解決安全執行緒問題。只需在run()方法前加上synshronized關鍵字即可。

 public synchronized void run() {   // .... }

三.同步代碼塊

正如上面所分析的那樣,解決安全執行緒問題其實只需限制對共用資源訪問的不確定性即可。使用同步方法時,使得整個方法體都成為了同步執行狀態,會使得可能出現同步範圍過大的情況,於是,針對需要同步的代碼可以直接另一種同步方式——同步代碼塊來解決。

同步代碼塊的格式為:

 synchronized (obj) {      //...  }

其中,obj為鎖對象,因此,選擇哪一個對象作為鎖是至關重要的。一般情況下,都是選擇此共用資源對象作為鎖對象。

如上例中,最好選用account對象作為鎖對象。(當然,選用this也是可以的,那是因為建立線程使用了runnable方式,如果是直接繼承Thread方式建立的線程,使用this對象作為同步鎖會其實沒有起到任何作用,因為是不同的對象了。因此,選擇同步鎖時需要格外小心...)

四.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 com.qqyumidi;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()的用法。部分輸出結果為:

取錢線程 開始要執行wait操作 執行了wait操作 -- i=0
存錢線程 存款:700.0 -- i=0
存錢線程-- 存錢 -- 執行完畢 -- i=0
存錢線程 開始要執行wait操作 -- i=1
取錢線程 執行了wait操作 執行了wait操作 -- i=0
取錢線程 取錢:700.0 -- i=1
取錢線程-- 取錢 -- 執行完畢 -- i=1
取錢線程 開始要執行wait操作 執行了wait操作 -- i=2
存錢線程 執行了wait操作 -- i=1
存錢線程 存款:700.0 -- i=2
存錢線程-- 存錢 -- 執行完畢 -- i=2
取錢線程 執行了wait操作 執行了wait操作 -- i=2
取錢線程 取錢:700.0 -- i=3
取錢線程-- 取錢 -- 執行完畢 -- i=3
取錢線程 開始要執行wait操作 執行了wait操作 -- i=4
存錢線程 存款:700.0 -- i=3
存錢線程-- 存錢 -- 執行完畢 -- i=3
存錢線程 開始要執行wait操作 -- i=4
取錢線程 執行了wait操作 執行了wait操作 -- i=4
取錢線程 取錢:700.0 -- i=5
取錢線程-- 取錢 -- 執行完畢 -- i=5
取錢線程 開始要執行wait操作 執行了wait操作 -- i=6
存錢線程 執行了wait操作 -- i=4
存錢線程 存款:700.0 -- i=5
存錢線程-- 存錢 -- 執行完畢 -- i=5
存錢線程 開始要執行wait操作 -- i=6
取錢線程 執行了wait操作 執行了wait操作 -- i=6
取錢線程 取錢:700.0 -- i=7
取錢線程-- 取錢 -- 執行完畢 -- i=7
取錢線程 開始要執行wait操作 執行了wait操作 -- i=8
存錢線程 執行了wait操作 -- i=6
存錢線程 存款: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()方法的用法,但其本質上說,已經是一個簡單的生產者-消費者模式了。

 系列文章:

java 多線程執行個體講解 (一)
Java 多線程執行個體詳解(二)
Java 多線程執行個體詳解(三)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.