最近想將Java(Java教程 Java培訓 基礎的一些東西都整理整理,寫下來,這是對知識的總結,也是一種樂趣。已經擬好了提綱,大概分為這幾個主題: java線程安全,java垃圾收集,java并發包詳細介紹,java profile和jvm性能調優。慢慢寫吧。本人jameswxx原創文章,轉載請注明出處,我費了很多心血,多謝了。關于java線程安全,網上有很多資料,我只想從自己的角度總結對這方面的考慮,有時候寫東西是很痛苦的,知道一些東西,但想用文字說清楚,卻不是那么容易。我認為要認識java線程安全,必須了解兩個主要的點:java的內存模型,java的線程同步機制。特別是內存模型,java的線程同步機制很大程度上都是基于內存模型而設定的。后面我還會寫java并發包的文章,詳細總結如何利用java并發包編寫高效安全的多線程并發程序。暫時寫得比較倉促,后面會慢慢補充完善。

  淺談java內存模型

  不同的平臺,內存模型是不一樣的,但是jvm的內存模型規范是統一的。其實java的多線程并發問題最終都會反映在java的內存模型上,所謂線程安全無非是要控制多個線程對某個資源的有序訪問或修改。總結java的內存模型,要解決兩個主要的問題:可見性和有序性。我們都知道計算機有高速緩存的存在,處理器并不是每次處理數據都是取內存的。JVM定義了自己的內存模型,屏蔽了底層平臺內存管理細節,對于java開發人員,要清楚在jvm內存模型的基礎上,如果解決多線程的可見性和有序性。

  那么,何謂可見性?多個線程之間是不能互相傳遞數據通信的,它們之間的溝通只能通過共享變量來進行。Java內存模型(JMM)規定了jvm有主內存,主內存是多個線程共享的。當new一個對象的時候,也是被分配在主內存中,每個線程都有自己的工作內存,工作內存存儲了主存的某些對象的副本,當然線程的工作內存大小是有限制的。當線程操作某個對象時,執行順序如下:

  (1)從主存復制變量到當前工作內存(read and load)

  (2)執行代碼,改變共享變量值(use and assign)

  (3)用工作內存數據刷新主存相關內容(store and write)

  JVM規范定義了線程對主存的操作指令:read,load,use,assign,store,write.當一個共享變量在多個線程的工作內存中都有副本時,如果一個線程修改了這個共享變量,那么其他線程應該能夠看到這個被修改后的值,這就是多線程的可見性問題。

  那么,什么是有序性呢?線程在引用變量時不能直接從主內存中引用,如果線程工作內存中沒有該變量,則會從主內存中拷貝一個副本到工作內存中,這個過程為read-load,完成后線程會引用該副本。當同一線程再度引用該字段時,有可能重新從主存中獲取變量副本(read-load-use),也有可能直接引用原來的副本(use),也就是說 read,load,use順序可以由JVM實現系統決定。

  線程不能直接為主存中中字段賦值,它會將值指定給工作內存中的變量副本(assign),完成后這個變量副本會同步到主存儲區(store-write),至于何時同步過去,根據JVM實現系統決定。有該字段,則會從主內存中將該字段賦值到工作內存中,這個過程為read-load,完成后線程會引用該變量副本,當同一線程多次重復對字段賦值時,比如:

  Java代碼

  for(int i=0;i<10;i++)

  a++;線程有可能只對工作內存中的副本進行賦值,只到最后一次賦值后才同步到主存儲區,所以assign,store,weite順序可以由JVM實現系統決定。假設有一個共享變量x,線程a執行x=x+1.從上面的描述中可以知道x=x+1并不是一個原子操作,它的執行過程如下:

  1、從主存中讀取變量x副本到工作內存2、給x加1 3、將x加1后的值寫回主 存

  如果另外一個線程b執行x=x-1,執行過程如下:

  1、從主存中讀取變量x副本到工作內存2、給x減1 3、將x減1后的值寫回主存

  那么顯然,最終的x的值是不可靠的。假設x現在為10,線程a加1,線程b減1,從表面上看,似乎最終x還是為10,但是多線程情況下會有這種情況發生:

  1:線程a從主存讀取x副本到工作內存,工作內存中x值為10 2:線程b從主存讀取x副本到工作內存,工作內存中x值為10 3:線程a將工作內存中x加1,工作內存中x值為11 4:線程a將x提交主存中,主存中x為11 5:線程b將工作內存中x值減1,工作內存中x值為9 6:線程b將x提交到中主存中,主存中x為9

  同樣,x有可能為11,如果x是一個銀行賬戶,線程a存款,線程b扣款,顯然這樣是有嚴重問題的,要解決這個問題,必須保證線程a和線程b是有序執行的,并且每個線程執行的加1或減1是一個原子操作。看看下面代碼:

  Java代碼

  public class Account {

  private int balance;

  public Account(int balance) { this.balance = balance;}

  public int getBalance() { return balance;}

  public void add(int num) { balance = balance + num;}

  public void withdraw(int num) { balance = balance - num;}

  public static void main(String[] args) throws InterruptedException { Account account = new Account(1000);Thread a = new Thread(new AddThread(account, 20), "add");Thread b = new Thread(new WithdrawThread(account, 20), "withdraw");a.start();b.start();a.join();b.join();System.out.println(account.getBalance());}

  static class AddThread implements Runnable { Account account;int amount;

  public AddThread(Account account, int amount) { this.account = account;this.amount = amount;}

  public void run() { for (int i = 0; i < 200000; i++) { account.add(amount);}

  static class WithdrawThread implements Runnable { Account account;int amount;

  public WithdrawThread(Account account, int amount) { this.account = account;this.amount = amount;}

  public void run() { for (int i = 0; i < 100000; i++) { account.withdraw(amount);}第一次執行結果為10200,第二次執行結果為1060,每次執行的結果都是不確定的,因為線程的執行順序是不可預見的。這是java同步產生的根源,synchronized關鍵字保證了多個線程對于同步塊是互斥的,synchronized作為一種同步手段,解決java多線程的執行有序性和內存可見性,而volatile關鍵字之解決多線程的內存可見性問題。后面將會詳細介紹。