Java線程(一)

來源:互聯網
上載者:User

標籤:minor   hmm   erb   描述   dnw   入棧   tde   memory   roi   

1. java什麼叫安全執行緒?什麼叫不安全?

就是線程同步的意思,就是當一個程式對一個安全執行緒的方法或者語句進行訪問的時候,其他的不能再對他進行操作了,必須等到這次訪問結束以後才能對這個安全執行緒的方法進行訪問
什麼叫安全執行緒:
如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程啟動並執行結果是一樣的,而且其他的變數的值也和預期的是一樣的,
就是安全執行緒的。
或者說:一個類或者程式所提供的介面對於線程來說是原子操作或者多個線程之間的切換不會導致該介面的執行結果存在二義性,也就是說我們不用考慮同步的問題。
安全執行緒問題都是由全域變數及靜態變數引起的。
若每個線程中對全域變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全域變數是安全執行緒的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則就可能影響安全執行緒。

 

存在競爭的線程不安全,不存在競爭的線程就是安全的

 

1.安全執行緒(局部變數)

    public static void test1() {        int i = 0;        i++;        System.out.println(i);    }

 

  由於i是局部變數,不存線上程安全的問題。100個線程同時訪問,每個線程都有自己的i,不會產生安全執行緒問題。

 

2.servlet線程不安全(全域變數)

  servlet對象只有一個,i也只有一個,存線上程安全問題。

  靜態成員變數也會存線上程安全,靜態成員在類載入器載入的時候執行,多個對象共用同一個靜態成員變數,且靜態成員變數不會被釋放,Java對象會被JVM自動回收,因此靜態成員變數需要謹慎使用。比如一個靜態list集合,一個伺服器啟動後會開啟多年,list也會存在多年,如果對象向list中添加資料會導致崩潰(list過大)。

 

2.    淺談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代碼  

  1. for(int i=0;i<10;i++)   
  2.  a++;  

 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是一個原子操作。

synchronized關鍵字
        java用synchronized關鍵字做為多線程並發環境的執行有序性的保證手段之一。當一段代碼會修改共用變數,這一段代碼成為互斥區或臨界區,為了保證共用變數的正確性,synchronized標示了臨界區。典型的用法如下:

Java代碼  

  1. synchronized(鎖){   
  2.      臨界區代碼   
  3. }   

synchronized(鎖){

     臨界區代碼

}

 

理論上,每個對象都可以做為鎖,但一個對象做為鎖時,應該被多個線程共用,這樣才顯得有意義,在並發環境下,一個沒有共用的對象作為鎖是沒有意義的。假如有這樣的代碼:

Java代碼  

  1. public class ThreadTest{   
  2.   public void test(){   
  3.      Object lock=new Object();   
  4.      synchronized (lock){   
  5.         //do something   
  6.      }   
  7.   }   
  8. }  

public class ThreadTest{

  public void test(){

     Object lock=new Object();

     synchronized (lock){

        //do something

     }

  }

}

 


lock變數作為一個鎖存在根本沒有意義,因為它根本不是共用對象,每個線程進來都會執行Object lock=new Object();每個線程都有自己的lock,根本不存在鎖競爭。
        每個鎖對象都有兩個隊列,一個是就緒隊列,一個是阻塞隊列,就緒佇列儲存體了將要獲得鎖的線程,阻塞佇列儲存體了被阻塞的線程,當一個被線程被喚醒 (notify)後,才會進入到就緒隊列,等待cpu的調度。當一開始線程a第一次執行account.add方法時,jvm會檢查鎖對象account 的就緒隊列是否已經有線程在等待,如果有則表明account的鎖已經被佔用了,由於是第一次運行,account的就緒隊列為空白,所以線程a獲得了鎖,執行account.add方法。如果恰好在這個時候,線程b要執行account.withdraw方法,因為線程a已經獲得了鎖還沒有釋放,所以線程 b要進入account的就緒隊列,等到得到鎖後才可以執行。
一個線程執行臨界區代碼過程如下:
1 獲得同步鎖
2 清空工作記憶體
3 從主存拷貝變數副本到工作記憶體
4 對這些變數計算
5 將變數從工作記憶體寫回到主存
6 釋放鎖
可見,synchronized既保證了多線程的並發有序性,又保證了多線程的記憶體可見度。

 

可能 很多人都覺得莫名其妙,說JVM的記憶體模型,怎麼會扯到cpu上去呢?在此,我認為很有必要闡述下,免得很多人看得不明不白的。先拋開java虛擬機器不 談,我們都知道,現在的電腦,cpu在計算的時候,並不總是從記憶體讀取資料,它的資料讀取順序優先順序是:寄存器-快取-記憶體。線程耗費的是CPU, 線程計算的時候,原始的資料來自記憶體,在計算過程中,有些資料可能被頻繁讀取,這些資料被儲存在寄存器和快取中,當線程計算完後,這些緩衝的資料在適 當的時候應該寫回記憶體。當個多個線程同時讀寫某個記憶體資料時,就會產生多線程並發問題,涉及到三個特性:原子性,有序性,可見度。在《安全執行緒總結》這篇 文章中,為了理解方便,我把原子性和有序性統一叫做“多線程執行有序性”。支援多線程的平台都會面臨這種問題,運行在多線程平台上支援多線程的語言應該提 供解決該問題的方案。

 

       那麼,我們看看JVM,JVM是一個虛擬電腦,它也會面臨多線程並發問題,java程式運行在java虛擬機器平台上,java程式員不可能直接去控制 底層線程對寄存器快取記憶體之間的同步,那麼java從文法層面,應該給開發人員提供一種解決方案,這個方案就是諸如 synchronized, volatile,鎖機制(如同步塊,就緒隊列,阻塞隊列)等等。這些方案只是文法層面的,但我們要從本質上去理解它,不能僅僅知道一個 synchronized 可以保證同步就完了。   在這裡我說的是jvm的記憶體模型,是動態,面向多線程並發的,沿襲JSL的“working memory”的說法,只是不想牽扯到太多底層細節,因為《安全執行緒總結》這篇文章意在說明怎樣從文法層面去理解java的線程同步,知道各個關鍵字的使用情境。

 

      今天有人問我,那java的線程不是有棧嗎?難道棧不是工作記憶體嗎?工作記憶體這四個字得放到具體的情境中描述,方能體現它具體的意義,在描述JVM的線程 同步時,工作記憶體指的是寄存器和告訴緩衝的抽象描述,具體請自行參閱JLS。上面講的都是動態記憶體模型,甚至已經超越了JVM的範圍,那麼JVM的記憶體 靜態儲存是怎麼劃分的?今天還有人問我,jvm的記憶體模型不是有eden區嗎?也不見你提起。我跟他說,這是兩個角度去看的,甚至是兩個不同的範圍,動態 的線程同步的記憶體模型,涵蓋了cpu,寄存器,快取,記憶體;JVM的靜態記憶體儲模型只是一種對記憶體的物理劃分而已,它只局限在記憶體,而且只局限在 JVM的記憶體。那些什麼線程棧,eden區都僅僅在JVM記憶體。

 

      說說JVM的線程棧和有個朋友反覆跟我糾結的eden區吧。JVM的記憶體,被劃分了很多的地區:

1.程式計數器
每一個Java線程都有一個程式計數器來用於儲存程式執行到當前方法的哪一個指令。
2.線程棧
線程的每個方法被執行的時候,都會同時建立一個幀(Frame)用於儲存本地變數表、操作棧、動態連結、方法出入口等資訊。每一個方法的調用至完成,就意味著一個幀在VM棧中的入棧至出棧的過程。如果線程請求的棧深度大於虛擬機器所允許的深度,將拋出StackOverflowError異常;如果VM棧可以動態擴充(VM Spec中允許固定長度的VM棧),當擴充時無法申請到足夠記憶體則拋出OutOfMemoryError異常。
3.本地方法棧
4.堆

每個線程的棧都是該線程私人的,堆則是所有線程共用的。當我們new一個對象時,該對象就被分配到了堆中。但是堆,並不是一個簡單的概念,堆區又劃分了很多地區,為什麼堆劃分成這麼多地區,這是為了JVM的記憶體垃圾收集,似乎越扯越遠了,扯到垃圾收集了,現在的jvm的gc都是按代收集,堆區大致被分為三大塊:新生代,舊生代,持久代(虛擬);新生代又分為eden區,s0區,s1區。建立一個對象時,基本小的對象,生命週期短的對象都會放在新生代的eden區中,eden區滿時,有一個小範圍的gc(minor gc),整個新生代滿時,會有一個大範圍的gc(major gc),將新生代裡的部分對象轉到舊生代裡。
5.方法區
其實就是永久代(Permanent Generation),方法區中存放了每個Class的結構資訊,包括常量池、欄位描述、方法描述等等。VM Space描述中對這個地區的限制非常寬鬆,除了和Java堆一樣不需要連續的記憶體,也可以選擇固定大小或者可擴充外,甚至可以選擇不實現垃圾收集。相對來說,垃圾收集行為在這個地區是相對比較少發生的,但並不是某些描述那樣永久代不會發生GC(至 少對當前主流的商業JVM實現來說是如此),這裡的GC主要是對常量池的回收和對類的卸載,雖然回收的“成績”一般也比較差強人意,尤其是類卸載,條件相當苛刻。
6.常量池
 Class檔案中除了有類的版本、欄位、方法、介面等描述等資訊外,還有一項資訊是常量表(constant_pool table),用於存放編譯期已可知的常量,這部分內容將在類載入後進入方法區(永久代)存放。但是Java語言並不要求常量一定只有編譯期預置入Class的常量表的內容才能進入方法區常量池,運行期間也可將新內容放入常量池(最典型的String.intern()方法)。

 

 

3.一個線程不安全的例子

  當我們查看JDK API的時候,總會發現一些類說明寫著,安全執行緒或者線程不安全,比如說StringBuilder中,有這麼一句,“將StringBuilder 的執行個體用於多個線程是不安全的。如果需要這樣的同步,則建議使用StringBuffer。 ”,那麼下面手動建立一個線程不安全的類,然後在多線程中使用這個類,看看有什麼效果。

        Count.java:

 

 public class Count {      private int num;      public void count() {          for(int i = 1; i <= 10; i++) {              num += i;          }          System.out.println(Thread.currentThread().getName() + "-" + num);      }  } 

 

  在這個類中的count方法是計算1一直加到10的和,並輸出當前線程名和總和,我們期望的是每個線程都會輸出55。

ThreadTest.java:

 

 public class ThreadTest {      public static void main(String[] args) {          Runnable runnable = new Runnable() {              Count count = new Count();              public void run() {                  count.count();              }          };          for(int i = 0; i < 10; i++) {              new Thread(runnable).start();          }      }  } 

 

 

 

 

這裡啟動了10個線程,看一下輸出結果:

 

 Thread-0-55  Thread-1-110  Thread-2-165  Thread-4-220  Thread-5-275  Thread-6-330  Thread-3-385  Thread-7-440  Thread-8-495  Thread-9-550

 

 

 

只有Thread-0線程輸出的結果是我們期望的,而輸出的是每次都累加的,這裡累加的原因以後的博文會說明,那麼要想得到我們期望的結果,有幾種解決方案:

1. 將Count中num變成count方法的局部變數(局部變數不存線上程安全問題)

 public class Count {      public void count() {          int num = 0;          for(int i = 1; i <= 10; i++) {              num += i;          }          System.out.println(Thread.currentThread().getName() + "-" + num);      }  }

 

2. 將線程類成員變數拿到run方法中,這時count引用是線程內的局部變數;

 public class ThreadTest4 {      public static void main(String[] args) {          Runnable runnable = new Runnable() {              public void run() {                  Count count = new Count();                  count.count();              }          };          for(int i = 0; i < 10; i++) {              new Thread(runnable).start();          }      }  } 

3. 每次啟動一個線程使用不同的線程類,不推薦。

  上述測試,我們發現,存在成員變數的類用於多線程時是不安全的,不安全體現在這個成員變數可能發生非原子性的操作, 而變數定義在方法內也就是局部變數是安全執行緒的。想想在使用struts1時,不推薦建立成員變數,因為action是單例的,如果建立了成員變數,就會 存線上程不安全的隱患,而struts2是每一次請求都會建立一個action,就不用考慮安全執行緒的問題。所以,日常開發中,通常需要考慮成員變數或者 說全域變數在多線程環境下,是否會引發一些問題。

 

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.