淺談利用同步機制解決Java中的線程安全問題
阿新 • • 發佈:2017-07-02
顯示 重要 false 希望 運行程序 obj balance urn 什麽
我們知道大多數程序都不會是單線程程序,單線程程序的功能非常有限,我們假設一下所有的程序都是單線程程序,那麽會帶來怎樣的結果呢?假如淘寶是單線程程序,一直都只能一個一個用戶去訪問,你要在網上買東西還得等著前面千百萬人挑選購買,最後心儀的商品下架或者售空......假如餓了嗎是單線程程序,那麽一個用戶得等前面全國千萬個用戶點完之後才能進行點餐,那餓了嗎就該倒閉了不是嗎?以上兩個簡單的例子,就說明一個程序能進行多線程並發訪問的重要性,今天就讓我們去了解一下Java中多線程並發訪問這個方向吧。 **第一步:理解多線程的概念** 很多初學者,並不知道什麽是多線程,那麽在此我將簡單介紹一下多線程。線程是指在一個進程中的一個順序執行流(也就是一段執行的代碼),多線程則是在一個進程中存在多個順序執行流,它們相互獨立,共享進程中的所有資源(進程中的代碼段,進程的內存空間等)。 **第二步:多線程的並發訪問** 那麽多個線程又是如何進行並發訪問的呢?我們直接上代碼: public class MyThread extends Thread{ public void run(){ for(int i=0;i<5;i++){ System.out.println(Thread.currentThread().getName()+" "+i); } } public static void main(String[] args) { for(int i=0;i<5;i++){ System.out.println(Thread.currentThread().getName()+" "+i); if(i==3){ new MyThread().start(); new MyThread().start(); } } } 上面就是一段最簡單的多線程並發執行的例子,這段代碼中一共有三個線程,main、Thread-0、Thread-1。它們並發執行上面的程序。我們來看看運行的結果: main 2 main 3 Thread-0 0 Thread-1 0 main 4 Thread-1 1 Thread-1 2 Thread-0 1 Thread-0 2 以上是我截取的部分運行結果,由運行結果可以看出main、 Thread-0、Thread-1三個線程都對i進行了取值,而且三個線程相對獨立,各自取各自的值,相互不影響。同時應該註意到,三個線程取的值是不連續的,這是因為我所創建的i是一個實例變量而不是一個局部變量,每個線程去執行線程執行體的時候都會重新對i進行取值,所以此處對i的取值不是連續的。 對於上述代碼和運行結果可知,多線程並發訪問的特點是:線程之間相互獨立,不受其他線程的幹擾。 **第三步:多線程並發訪問時同步安全問題** 從第二步的敘述中,我們知道了多個線程可以對一個對象進行同時訪問,那麽一些問題也隨之出現,那就是多線程並發訪問一個對象時的安全問題。我們由一個經典題目來慢慢去剖析多線程並發安全問題,並嘗試去解決這個問題。 銀行取錢問題:銀行取錢的流程我們大概可以分為這麽幾步: *1. 用戶輸入賬戶、密碼,系統去判斷用戶的賬戶密碼是否正確。 *2. 用戶輸入取款金額。 *3. 系統判斷用戶的余額是否大於用戶的取款金額。 *4. 如果用戶的余額大於取款金額,則取款成功,如果用戶的余額小於取款金額,則取款失敗。 上面的操作結果好像是有理有據的,那麽我們就繼續上代碼去完成上面的需求吧! class Account{ //封裝用戶的賬戶、密碼 private String account; private double balance; public Account(String account,double balance){ this.balance = balance; this.account = account; } public void setAccount(String account) { this.account = account; } public String getAccount() { return account; } public void setBalance(double balance) { this.balance = balance; } public double getBalance() { return balance; } public int hashcode(){ return account.hashCode(); } @Override public boolean equals(Object obj) { if(this == obj){ return true; } if(this != obj && obj.getClass()== Account.class){ Account a = (Account)obj; return a.getAccount().equals(account); } return false; } } class DrawAccount extends Thread{ //模擬用戶的賬戶 private Account account; //獲取當前希望取的錢數 private double drawAccount; private String name; public DrawAccount( Account account,double drawAccount) { this.account = account; this.drawAccount = drawAccount; } @Override public void run() { //余額大於取錢的數目 if(account.getBalance()>=drawAccount){ System.out.println("您的名字是"+getName()+" "+"您要提取的現金為:"+drawAccount+"元"); account.setBalance(account.getBalance()-drawAccount); System.out.println("您的余額為:"+account.getBalance()+"元"); } else{ System.out.println("您輸入的金額有誤,取錢失敗"); } } } public class Drawtext{ public static void main(String[] args) { Account a = new Account("12345", 1500); //現在就模擬兩個線程去對同一個賬戶同時取錢 new DrawAccount(a,1000).start(); new DrawAccount(a,1000).start(); } } 上面的代碼我開啟了兩個線程同時取錢,並且完全符合我前面所述的銀行取錢流程,那麽現在我們運行這個程序: 您的名字是 Thread-0您要提取的現金為:1000.0元 您的余額為:500.0 您的名字是 Thread-1您要提取的現金為:1000.0元 您的余額為:-500.0 由上面的運行結果可知,當兩個用戶(線程)同時取錢的時候,程序就會出現差錯,這是與銀行系統的需求不匹配的,所以我們要對程序的bug作出分析,並作出相應的修改。 通過分析可知,我們必須控制在相同的時刻只能有一個用戶取錢(也就是說,只能有一個線程對余額進行訪問),這個時候,我們就提出了線程安全問題,解決銀行多客戶對同一賬戶並發取錢問題,就是要去解決線程安全問題。 線程安全問題的感念:如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。 線程安全問題的常用解決辦法: 1、利用同步機制去解決多線程並發訪問而造成的線程安全問題:同步代碼塊、同步監視器、同步鎖。 2、創建不可變類(對象、方法等)。 今天我們主要講解利用同步機制去解決Java中的線程安全問題 我們還是從概念出發: 同步:同步指兩個或兩個以上隨時間變化的量在變化過程中保持一定的相對關系。同步(英語:Synchronization),指在一個系統中所發生的事件,之間進行協調,在時間上出現一致性與統一化的現象。在系統中進行同步,也被稱為及時、同步化的。 同步代碼塊:同步代碼塊是利用了同步監視器來解決線程同步問題。同步代碼塊的格式如下: Synchronized(obj){ ....... //此處就是同步代碼塊 } 上面的格式中:obj是同步監視器,通常由可能被並發訪問的共享資源來充當同步監視器。 那麽如果我們要用同步代碼快去解決上面銀行取錢問題,怎麽去做呢?很簡單,我們繼續上代碼: class DrawAccount extends Thread{ //模擬用戶的賬戶 private Account account; //獲取當前希望取的錢數 private double drawAccount; private String name; public DrawAccount( Account account,double drawAccount) { this.account = account; this.drawAccount = drawAccount; } @Override public void run() { //這裏我們必須讓account來充當同步監視器,任何線程在執行同步代碼快之前都要將同步監視器進行鎖定,被鎖定的同步監視器,只能由這一個線程去訪問,其他線程無法訪問,只有當該線程釋放了對同步監視器的鎖定之後,其他的線程才擁有訪問同步監視器的資格。 Synchronized(account){ //余額大於取錢的數目 if(account.getBalance()>=drawAccount){ System.out.println("您的名字是"+getName()+" "+"您要提取的現金為:"+drawAccount+"元"); account.setBalance(account.getBalance()-drawAccount); System.out.println("您的余額為:"+account.getBalance()+"元"); } else{ System.out.println("您輸入的金額有誤,取錢失敗"); } } //同步代碼塊結束,線程釋放對同步監視器的鎖定。 } } 使用同步代碼塊去解決問題之後,我們運行上面的代碼,看看效果如何? 您的名字是 Thread-0您要提取的現金為:1000.0元 您的余額為:500.0 您輸入的金額有誤,取錢失敗 果不其然,再利用同步代碼快對程序進行修改之後,我們的問題也迎刃而解! 下面,我們再用同步方法去解決問題。 同步方法其實很簡單,就是使用synchronized去修飾一個方法,格式如下: public synchronized void draw(){} 上面就是同步方法的標準格式,現在我們用同步方法去解決銀行取錢問題。上代碼: public synchronized void draw(double drawAmount) { //余額大於取錢的數目 if(account.getBalance()>=drawAccount){ System.out.println("您的名字是"+getName()+" "+"您要提取的現金為:"+drawAccount+"元"); account.setBalance(account.getBalance()-drawAccount); System.out.println("您的余額為:"+account.getBalance()+"元"); } else{ System.out.println("您輸入的金額有誤,取錢失敗"); } } } 我們將涉及到余額修改的方法改成同步方法,運行程序: 您的名字是 Thread-0您要提取的現金為:1000.0元 您的余額為:500.0 您輸入的金額有誤,取錢失敗 我們發現,用同步方法去修改,也能解決問題。那麽最後一個方法能否行得通呢?我們來試試吧! 同步鎖:我們這裏寫的同步鎖,是ReentrantLock(可重入鎖),使用該鎖對象,可以顯示的加鎖,釋放鎖。通常使用ReentrantLock的格式如下: class A{ private final ReentrantLock lock = new ReentrantLock(); //... //定義需要保證線程安全的方法 public void M(){ //加鎖 lock.lock(); try{ //需要保證安全的代碼.... } //使用finally塊來釋放鎖 finally{ lock.unlock(); } } } 這裏出現了finally塊,通常我建議大家用finally塊來保證鎖的釋放。 現在我們用同步鎖來修改程序。上代碼: public class Account{ private final ReentrantLock lock = new ReentrantLock(); //模擬用戶的賬戶 private Account account; //獲取當前希望取的錢數 private double drawAccount; private String name; public DrawAccount( Account account,double drawAccount) { this.account = account; this.drawAccount = drawAccount; } public void setAccount(String account) { this.account = account; } public String getAccount() { return account; } //因為不允許余額隨便更改,所以我們只設定了balance的get方法 public double getBalance() { return balance; } //提供一個線程安全的draw()方法去完成取錢的操作 public void draw(double drawAmount){ //加鎖 lock.lock(); try{ if(balance>=drawAmount){ System.out.println(Thread.currenThread().getName()+"您要提取的現金為:"+drawAccount+"元"); //修改余額 balance-=drawAmount; System.out.println("您的余額為:"+balance+"元"); } else{ System.out.println("您輸入的金額有誤,取錢失敗"); } finally{//使用finally塊來釋放鎖 lock.unlock(); } //省略hashcode()和equals()方法 此處我們使用了一個同步鎖來對取錢的相關的代碼進行鎖定,運行結果: Thread-0您要提取的現金為:1000.0元 您的余額為:500.0元 您輸入的金額有誤,取錢失敗 從上面的運行結果可以出,使用同步鎖也能防止多線程並發訪問而造成的線程安全問題。 好啦,今天向大夥兒通過銀行取錢案例介紹了三種同步方式去解決線程安全問題,相信大家都對三種方法有了以一定的了解,希望我的博客能對大家有所收獲。加油啦! (備註:因本人能力有限,在寫博客的時候難免有所疏漏,如有缺漏之處,懇請各位讀者諒解,並歡迎大家給我指出問題,讓我能向大家學到更多知識!) 雞年第一更!祝大家雞年快樂!
淺談利用同步機制解決Java中的線程安全問題