java多線程的難點是在:處理多個線程同步與並發運行時線程間的通信問題。java在處理線程同步時,常用方法有:
1、synchronized關鍵字。
2、Lock顯示加鎖。
3、信號量Semaphore。
線程同步問題引入:
創建一個銀行賬戶Account類,在創建並啟動100個線程往同一個Account類實例里面添加一塊錢。在沒有使用上面三種方法的情況下:
代碼:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class AccountWithoutSync { private static Account account = new Account(); //實例化一個賬戶 public static void main(String[] args) { long start = System.currentTimeMillis(); //使用ExecutorService創建線程池 ExecutorService executor = Executors.newCachedThreadPool(); for(int i=0;i<100;i++) { executor.execute(new AddPennyTask()); } //關閉線程池 即使線程池中還有未完成的線程 返回未完成的清單 executor.shutdown(); //關閉之后還是要保證未完成的線程繼續完成 如果線程池中所有任務都完成了,isTerminated返回true while(!executor.isTerminated()) {} long end = System.currentTimeMillis(); //balance有余額的意思 System.out.println("現在賬戶里面的余額是:" + account.getBalance()); System.out.println("花費的時間以微秒為單位:"+(end-start)+"微秒"); } //這個線程只調用了一個方法 public static class AddPennyTask implements Runnable { @Override public void run() { account.deposit(1); } } //一個內部類 用於 賬戶的相關處理 public static class Account { private int balance =0; public int getBalance() { return balance; } public void deposit(int amount) { int newBalance = balance + amount; //為了讓錯誤體現的更明顯 try { Thread.sleep(4); //5毫秒 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } balance= newBalance; //其實就是balance +=amount;//不過換成這一段代碼結果就在100和99左右 } } }
錯的不是一般的明顯,明明存入了100塊,顯示只有2塊,原因也很簡單,就是100個線程同時對acount進行修改,當第100該線程把錢改成100時,第2個線程最后修改,把它改成了2,這群不聽話的線程,那就好好管管他們吧,讓他們乖乖聽話,在這之前,還是要記住以下一些名詞:
競爭狀態:當多個線程訪問同一公共資源並引發沖突造成線程不同步,我們稱這種狀態為競爭狀態。
線程安全:是針對類來說的,如果一個類的對象在多線程程序中不會導致競爭狀態,我們就稱這類為線程安全的,上面的Account是線性不安全的,而又如StringBuffer是線程安全的,而StringBuilder則是線程不安全的。
臨界區:造成競爭狀態的原因是多個線程同時進入了程序中某一特定部分,如上面程序中的deposit方法,我們稱這部分為程序中的臨界區,我們要解決多線程不同步問題,就是要解決如何讓多個線程有序的訪問臨界區。
使用synchronized關鍵字
1、同步方法:在deposit方法前加synchronized關鍵字,使得該方法變成同步方法:public synchronized void deposit(double amount){}
2、同步語句:對某一代碼塊加synchronized關鍵字,常用格式:
synchronized(exper) {statement} 其中exper是對象的引用,如上面的程序,在要在調用depsoit方法時,改成這樣: synchronized(account){account.deposit(1);}
使用synchronized,其實是隱式地給方法或者代碼塊加了鎖,任何同步的實例方法都可以轉換為同步語句:
public synchronized void method(){}
轉換為同步語句: public void method{sysnchronized(this){}}
利用lock加鎖同步
java也可以用Lock顯示的對臨界區代碼加鎖以及解鎖,這比用synchronized關鍵字更加直觀靈活。
一個鎖是一個Lock接口的實例,該接口定義了加鎖解鎖的方法,且一個鎖可以多次調用其newCondition()方法創建名為Condition對象的實例,以此進行線程間的通信(在后面用到)。
有了Lock接口,我們還要實現它,java提供了RenentrantLock類,該類是為創建相互排斥鎖而實現了Lock接口,由此就好辦了,下面看一下書上的圖:
代碼如下:
public static class Account2 { private static Lock lock = new ReentrantLock(); private int balance =0; public int getBalance() { return balance; } public void deposit(int amount) { lock.lock(); try{ int newBalance = balance + amount; Thread.sleep(4); balance= newBalance; }catch (InterruptedException e) { e.printStackTrace(); } finally{ lock.unlock(); } } }
100塊存入,且時間明顯比之前久了,100個線程都乖乖的排隊訪問臨界區。另外注意在對lock()的調用后,緊跟隨try catch finnaly語句,這是個好習慣。
利用信號量同步
信號量是個好東西,信號量機制在操作系統方面有着廣泛的應用,如linux進程同步信號量,而在java里,Semaphore包包含了一些訪問信號量的方法。
信號量可以用來限制訪問共享資源的線程數,在訪問臨界區資源前,線程必須獲取一個信號量,在訪問完之后返回一個信號量。下圖是關於類Semaphore,該類包含訪問信號量的方法:
利用信號量Semaphorre,可以將上面的Account類改成這樣:
public static class Account
{ private static Semaphore semaphore = new Semaphore(1); //創建一個信號量 private int balance =0; public int getBalance() { return balance; } public void deposit(int amount) { try { semaphore.acquire(); int newBalance = balance + amount; Thread.sleep(4); balance= newBalance; } catch (InterruptedException e) { e.printStackTrace(); }finally{ semaphore.release(); //返回一個信號量 } } }
網上有很多關於java多線程的文章,有不少寫成系列的,但個人覺得像多線程這么有有意思的部分,還是得好好靜下心來品位,上面的三種方法,可以解決多線程同步問題,后面還會繼續學習,嗯,加油。