程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> Java中的synchronized

Java中的synchronized

編輯:JAVA綜合教程

Java中的synchronized


synchronized是針對對象的隱式鎖使用的,注意是對象!

舉個小例子,該例子沒有任何業務含義,只是為了說明synchronized的基本用法:

Java代碼收藏代碼
  1. ClassMyClass(){
  2. synchronizedvoidmyFunction(){
  3. //dosomething
  4. }
  5. }
  6. publicstaticvoidmain(){
  7. MyClassmyClass=newMyClass();
  8. myClass.myFunction();
  9. }
    好了,就這麼簡單。
    myFunction()方法是個同步方法,隱式鎖是誰的?答:是該方法所在類的對象。
    看看怎麼使用的:myClass.myFunction();很清楚了吧,隱式鎖是myClass的。
    說的在明白一點,線程想要執行myClass.myFunction();就要先獲得myClass的鎖。

    下面總結一下:
    1、synchronized關鍵字的作用域有二種:
    1)是某個對象實例內,synchronized aMethod(){}可以防止多個線程同時訪問這個對象的synchronized方法(如果一個對象有多個synchronized方法,只要一個線程訪問了其中的一個synchronized方法,其它線程不能同時訪問這個對象中任何一個synchronized方法)。這時,不同的對象實例的synchronized方法是不相干擾的。也就是說,其它線程照樣可以同時訪問相同類的另一個對象實例中的synchronized方法;

    2)是某個類的范圍,synchronized static aStaticMethod{}防止多個線程同時訪問這個類中的synchronized static 方法。它可以對類的所有對象實例起作用。(注:這個可以認為是對Class對象起作用)

    2、除了方法前用synchronized關鍵字,synchronized關鍵字還可以用於方法中的某個區塊中,表示只對這個區塊的資源實行互斥訪問。用法是: synchronized(this){/*區塊*/},它的作用域是this,即是當前對象。當然這個括號裡可以是任何對象,synchronized對方法和塊的含義和用法並不本質不同;

    3、synchronized關鍵字是不能繼承的,也就是說,基類的方法synchronized f(){} 在繼承類中並不自動是synchronized f(){},而是變成了f(){}。繼承類需要你顯式的指定它的某個方法為synchronized方法;

    synchronized可能造成死鎖,比如:
    Java代碼收藏代碼
    1. classDeadLockSample{
    2. publicfinalObjectlock1=newObject();
    3. publicfinalObjectlock2=newObject();
    4. publicvoidmethodOne(){
    5. synchronized(lock1){
    6. ...
    7. synchronized(lock2){...}
    8. }
    9. }
    10. publicvoidmethodTwo(){
    11. synchronized(lock2){
    12. ...
    13. synchronized(lock1){...}
    14. }
    15. }
    16. }
      假設場景:線程A調用methodOne(),獲得lock1的隱式鎖後,在獲得lock2的隱式鎖之前線程B進入運行,調用methodTwo(),搶先獲得了lock2的隱式鎖,此時線程A等著線程B交出lock2,線程B等著lock1進入方法塊,死鎖就這樣被創造出來了。

      下面舉一個有業務含義的例子幫助理解,並展示一下synchronized與wait()、notifyAll()的使用。
      這裡先介紹一下這兩個方法:
      wait()/notify():調用任意對象的 wait() 方法導致線程阻塞,並且該對象上的鎖被釋放。而調用任意對象的notify()方法則導致因調用該對象的 wait() 方法而阻塞的線程中隨機選擇的一個解除阻塞(但要等到獲得鎖後才真正可執行)。

      好了,再來看看synchronized與這兩個方法之間的關系:
      1.有synchronized的地方不一定有wait,notify

      2.有wait,notify的地方必有synchronized.這是因為wait和notify不是屬於線程類,而是每一個對象都具有的方法(事實上,這兩個方法是Object類裡的),而且,這兩個方法都和對象鎖有關,有鎖的地方,必有synchronized。
      慢著,讓我們思考一下Java這個設計是否合理?前面說了,鎖是針對對象的,wait()/notify()的操作是與對象鎖相關的,那麼把wait()/notify()設計在Object中也就是合情合理的了。
      恩,再想一下,為什麼有wait,notify的地方必有synchronized?
      synchronized方法中由當前線程占有鎖。另一方面,調用wait()notify()方法的對象上的鎖必須為當前線程所擁有。因此,wait()notify()方法調用必須放置在synchronized方法中,synchronized方法的上鎖對象就是調用wait()notify()方法的對象。若不滿足這一條件,則程序雖然仍能編譯,但在運行時會出現IllegalMonitorStateException 異常。

      好了,以上准備知識充足了,現在說例子:銀行轉賬,同一時刻只有一個人可以轉賬。
      那麼我們自然想到在Bank類中有一個同步的轉賬方法:
      Java代碼收藏代碼
      1. publicClassBank(){
      2. floataccount[ACCOUNT_NUM];
      3. ...
      4. publicsynchronizedvoidtransfer(from,to,amount){
      5. //轉賬
      6. }
      7. }
        現在有一個問題,如果一個人獲得了使用銀行的鎖,但是余額不足怎麼辦?
        好,那我們進行改進:
        Java代碼收藏代碼
        1. publicClassBank(){
        2. floataccount[ACCOUNT_NUM];
        3. ...
        4. publicsynchronizedvoidtransfer(intfrom,intto,floatamount){
        5. while(account[from]){
        6. wait();
        7. }
        8. account[from]-=amount;
        9. account[to]+=amount;
        10. notifyAll();
        11. }
        12. }
          這樣就滿足需求了。
          可見,用對象鎖來管理試圖進入synchronized方法的線程,
          另外,由條件判斷來管理已經進入同步方法中的線程即當前線程
          這裡還補充兩點:
          1. 調用wait()方法前的判斷最好用while,而不用if;因為while可以實現被喚醒後線程再次作條件判斷;而if則只能判斷一次
          2. 用notifyAll()優先於notify()。
          另外注意一點:
          能調用wait()/notify()的只有當前線程,前提是必須獲得了對象鎖,就是說必須要進入到synchronized方法中。

          -------------------------------------我是分割線----------------------------------------

          補充一點JMM的相關知識,對理解線程同步很有好處。
          (說明一下:以下內容參考了一些網上零零碎碎的帖子,非照搬且無商業目的,請勿跨省。)

          JVM中(留神:馬上講到的這兩個存儲區只在JVM內部與物理存儲區無關)存在一個主內存(Main Memory),Java中所有的變量存儲在主內存中,所有實例和實例的字段都在此區域,對於所有的線程是共享的(相當於黑板,其他人都可以看到的)。每個線程都有自己的工作內存(Working Memory),工作內存中保存的是主存中變量的拷貝,(相當於自己筆記本,只能自己看到),工作內存由緩存和堆棧組成,其中緩存保存的是主存中的變量的copy,堆棧保存的是線程局部變量。線程對所有變量的操作都是在工作內存中進行的,線程之間無法直接互相訪問工作內存,變量的值得變化的傳遞需要主存來完成。在JMM中通過並發線程修改的變量值,必須通過線程變量同步到主存後,其他線程才能訪問到。
          看看這個圖是不是更形象
          \

          好啦,下面來看線程對某個變量的操作步驟:
          1.從主內存中復制數據到工作內存
          2.執行代碼,對數據進行各種操作和計算
          3.把操作後的變量值重新寫回主內存中

          現在舉個例子,設想兩個棋手要通過兩個終端顯示器(Working Memory)對奕,而觀眾要通過服務器大屏幕(Main Memory )觀看他們的比賽過程。這兩個棋手相當於是同步中的線程,觀眾相當於其它線程。棋手是無法直接操作服務器的大屏幕的,他只能看到自己的終端顯示器,只能先從服務器上將當前結果先復制到自己的終端上(步驟1),然後在自己的終端上操作(步驟2),將操作的結果記錄在終端上,然後在某一時刻同步到服務器上(步驟3)。他所能看到的結果就是從服務器上復制到自己的終端上的內容,而要想把自己操作後的結果讓其他人看到必須同步到服務器上才行。至於什麼時候同步,那要看終端和服務器的通信機制。

          回到這三個步驟,這個順序是我們希望的,但是,JVM並不保證第1步和第3步會嚴格按照上述次序立即執行。因為根據java語言規范的規定,線程的工作內存和主存間的數據交換是松耦合的,什麼時候需要刷新工作內存或者什麼時候更新主存的內容,可以由具體的虛擬機實現自行決定。由於JVM可以對特征代碼進行調優,也就改變了某些運行步驟的次序的顛倒,那麼每次線程調用變量時是直接取自己的工作存儲器中的值還是先從主存儲器復制再取是沒有保證的,任何一種情況都可能發生。同樣的,線程改變變量的值之後,是否馬上寫回到主存儲器上也是不可保證的,也許馬上寫,也許過一段時間再寫。那麼,在多線程的應用場景下就會出現問題了,多個線程同時訪問同一個代碼塊,很有可能某個線程已經改變了某變量的值,當然現在的改變僅僅是局限於工作內存中的改變,此時JVM並不能保證將改變後的值立馬寫到主內存中去,也就意味著有可能其他線程不能立馬得到改變後的值,依然在舊的變量上進行各種操作和運算,最終導致不可預料的結果。

          這可如何是好呢?還好有synchronized和volatile:
          1.多個線程共有的字段應該用synchronized或volatile來保護.
          2.synchronized負責線程間的互斥.即同一時候只有一個線程可以執行synchronized中的代碼.
          synchronized還有另外一個方面的作用:在線程進入synchronized塊之前,會把工作存內存中的所有內容映射到主內存上,然後把工作內存清空再從主存儲器上拷貝最新的值。而在線程退出synchronized塊時,同樣會把工作內存中的值映射到主內存,不過此時並不會清空工作內存。這樣一來就可以強制其按照上面的順序運行,以保證線程在執行完代碼塊後,工作內存中的值和主內存中的值是一致的,保證了數據的一致性!
          3.volatile負責線程中的變量與主存儲區同步.但不負責每個線程之間的同步.
          volatile的含義是:線程在試圖讀取一個volatile變量時,會從主內存區中讀取最新的值。現在很清楚了吧。

          -------------------------------------我也是分割線----------------------------------------

          說到synchronized,那就再來談談ThreadLocal。
          在JDK的API文檔中ThreadLocal的定義第一句道出:This class provides thread-local variables. 好,這個類提供了線程本地的變量。只看這一句,讓我們結合到上面JMM的知識我們來分析一下理一下頭緒:
          我們已經知道了synchronized的含義是同步,也就是針對的是主存中的變量,只不過多線程執行時為了實現同步就需要每個線程在操作這個變量時要完成那三個步驟(對,就是主存與線程工作內存之間完成交互的那三步),我們很自然想到:
          1. 使用目的:需要有某些變量在多個線程中共享,有共享才會需要同步。
          2. 執行效率:直觀上感覺一下,同步的執行效率肯定不高,事實上也的確是這樣,為什麼?看看那三步多麻煩。
          好,現在再來看看ThreadLocal的定義,我們能想到什麼?
          首先讓我們思考一個問題,並不是所有多線程程序都需要共享啊,這個時候還用同步那一套豈不是很多余?讓每個線程維護自己的變量不就OK了,反正又不需要共享。對,ThreadLocal就是干這個事的。另一方面,那不用多說,性能上肯定優越喽。
          小結一下:對比synchronized和ThreadLocal首先要清楚,兩者的使用目的不同,關鍵點就在是否需要共享變量。就是說,ThreadLocal根本不是同步。再說啰嗦一點:ThreadLocal和Synchonized都用於解決多線程並發訪問。但是ThreadLocal與synchronized有本質的區別,synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal為每一個線程都提供了變量的副本,使得每個線程在某一時間訪問到的並不是同一個對象,這樣就隔離了多個線程對數據的數據共享。Synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。兩者處於不同的問題域。這個都不清晰的話說再多都沒用,只會更糊塗。

          好了,上個例子看看ThreadLocal怎麼用的(注:該例子來源於http://www.iteye.com/topic/81936這篇老帖子還是很值得看的):
          Java代碼收藏代碼
          1. publicclassThreadLocalDemoimplementsRunnable{
          2. privatefinalstaticThreadLocalstudentLocal=newThreadLocal();//ThreadLocal對象在這
          3. publicstaticvoidmain(String[]agrs){
          4. TreadLocalDemotd=newTreadLocalDemo();
          5. Threadt1=newThread(td,"a");
          6. Threadt2=newThread(td,"b");
          7. t1.start();
          8. t2.start();
          9. }
          10. publicvoidrun(){
          11. accessStudent();
          12. }
          13. publicvoidaccessStudent(){
          14. StringcurrentThreadName=Thread.currentThread().getName();
          15. System.out.println(currentThreadName+"isrunning!");
          16. Randomrandom=newRandom();
          17. intage=random.nextInt(100);
          18. System.out.println("thread"+currentThreadName+"setageto:"+age);
          19. Studentstudent=getStudent();//每個線程都獨立維護一個Student變量
          20. student.setAge(age);
          21. System.out.println("thread"+currentThreadName+"firstreadageis:"+student.getAge());
          22. try{
          23. Thread.sleep(5000);
          24. }
          25. catch(InterruptedExceptionex){
          26. ex.printStackTrace();
          27. }
          28. System.out.println("thread"+currentThreadName+"secondreadageis:"+student.getAge());
          29. }
          30. protectedStudentgetStudent(){
          31. Studentstudent=(Student)studentLocal.get();//從ThreadLocal對象中取
          32. if(student==null){
          33. student=newStudent();
          34. studentLocal.set(student);//如果沒有就創建一個
          35. }
          36. returnstudent;
          37. }
          38. protectedvoidsetStudent(Studentstudent){
          39. studentLocal.set(student);//放入ThreadLocal對象中
          40. }
          41. }
            ThreadLocal通過一個Map來為每個線程都持有一個變量副本,用ThreadLocal對象以鍵值對的方式來維護這些線程獨立變量 。

            -------------------------------呦這麼巧,我也是分割線----------------------------------

            既然說到了synchronized,順便說說ReentrantLock吧。
            ReentrantLock不熟悉?沒事,concurrent包裡的ArrayBlockingQueue知道吧,去看看源碼,發現了吧,裡面全是ReentrantLock。
            好,言歸正傳,ReentrantLock是何方神聖?先這麼說吧,你可以認為ReentrantLock是具有和synchronized類似功能的性能功能加強版同步鎖。
            讓我們先來看看synchronized有什麼缺點:
            1.只有一個condition與鎖相關聯,這個condition是什麼?就是synchronized對針對的對象鎖。
            2. 多線程競爭一個鎖時,其余未得到鎖的線程只能不停的嘗試獲得鎖,而不能中斷。這種情況對於大量的競爭線程會造成性能的下降等後果。

            針對synchronized的一系列缺點,JDK5提供了ReentrantLock,目的是為同步機制進行改善。下面來看看它是怎麼改善上面這兩個缺點的:
            1.一個ReentrantLock可以有多個Condition實例。
            舉個例子,還是剛才說的ArrayBlockingQueue類,看看源碼(節選):
            Java代碼收藏代碼
            1. publicclassArrayBlockingQueueextendsAbstractQueueimplementsBlockingQueue,java.io.Serializable{
            2. ...
            3. privatefinalReentrantLocklock;
            4. privatefinalConditionnotEmpty;
            5. privatefinalConditionnotFull;
            6. ...
            7. publicArrayBlockingQueue(intcapacity,booleanfair){
            8. ...
            9. lock=newReentrantLock(fair);
            10. notEmpty=lock.newCondition();
            11. notFull=lock.newCondition();//為該ReentrantLock設置了兩個Condition
            12. }
            13. publicEtake()throwsInterruptedException{
            14. finalReentrantLocklock=this.lock;
            15. lock.lockInterruptibly();
            16. try{
            17. try{
            18. while(count==0)
            19. notEmpty.await();//這裡針對notEmpty這個condition,如果隊列為空則線程等待這個條件
            20. }catch(InterruptedExceptionie){
            21. notEmpty.signal();
            22. throwie;
            23. }
            24. Ex=extract();
            25. returnx;
            26. }finally{
            27. lock.unlock();
            28. }
            29. }
            30. privateEextract(){
            31. finalE[]items=this.items;
            32. Ex=items[takeIndex];
            33. items[takeIndex]=null;
            34. takeIndex=inc(takeIndex);
            35. --count;
            36. notFull.signal();//這裡針對notFull這個condition,喚醒因該條件而等待的線程
            37. returnx;
            38. }
            39. ...
            40. }
              這裡notEmpty和notFull作為lock的兩個條件是可以分別負責管理想要加入元素的線程和想要取出元素的線程。例如put()方法在元素個數達到最大限制時會使用notFull條件把試圖繼續插入元素的線程都扔到等待集中,而執行了take()方法時如果順利進入extract()則會空出空間,這時notFull負責隨機的通知被其扔到等待集中的線程執行插入元素的操作。(這裡沒給出put方法,有興趣的童鞋可以去查查源碼,其實和take方法很類似)
              2. ReentrantLock提供了lockInterruptibly()方法可以優先考慮響應中斷,而不是像synchronized那樣不響應interrupt()操作。
              解釋一下響應中斷是什麼意思:比如A、B兩線程去競爭鎖,A得到了鎖,B等待,但是A有很多事情要處理,所以一直不返回。B可能就會等不及了,想中斷自己,不再等待這個鎖了,轉而處理其他事情。在這種情況下,synchronized的做法是,B線程中斷自己(或者別的線程中斷它),我不去響應,繼續讓B線程等待,你再怎麼中斷,我全當耳邊風。而lockInterruptibly()的做法是,B線程中斷自己(或者別的線程中斷它),ReentrantLock響應這個中斷,不再讓B等待這個鎖的到來。
              有了這個機制,使用ReentrantLock時就不會像synchronized那樣產生死鎖了。

              由於ReentrantLock在提供了多樣的同步功能(除了可響應中斷,還能設置時間限制),因此在同步比較激烈的情況下,性能比synchronized大大提高。
              不過,在同步競爭不激烈的情況下,synchronized還是非常合適的(因為JVM會進行優化,具體不清楚怎麼優化的)。因此不能說ReentrantLock一定更好,只是兩者適合情況不同而已,在同步競爭不激烈時用synchronized,激烈時用ReentrantLock。換句話說,ReentrantLock的可伸縮性可並發性要更好一些。除非您對 ReentrantLock的某個高級特性有明確的需要,或者有明確的證據(而不是僅僅是懷疑)表明在特定情況下,同步已經成為可伸縮性的瓶頸,否則還是應當繼續使用 synchronized。(這裡推薦一個帖子http://www.ibm.com/developerworks/cn/java/j-jtp10264/)

              再補充一點,使用ReentrantLock時,切記要在finally中釋放鎖,這是與synchronized使用方式很大的一個不同。對於synchronized,JVM會自動釋放鎖,而ReentrantLock需要你自己來處理。給個代碼片段吧:
              Java代碼收藏代碼
              1. //synchronized
              2. publicsynchronizedvoidincrement(){
              3. count++;
              4. }
                Java代碼收藏代碼
                1. //ReentrantLock
                2. publicvoidincrement(){
                3. lock.lockInterruptibly();//上鎖
                4. try{
                5. count++;
                6. }finally{
                7. lock.unlock();//手動釋放鎖
                8. }
                9. }

                  推薦一個帖子,講ReentrantLock還不錯我參考了的http://yanxuxin.iteye.com/blog/566713


                  總結:

                  synchronized是針對對象的隱式鎖使用的!

                  修飾實例方法時,競爭實例對象鎖。

                  修飾靜態方法時,競爭類對象鎖。

                  修飾代碼塊時,競爭參數對象鎖。

                  鎖是不能被繼承的。

                  wait()/notify():調用任意對象的 wait() 方法導致線程阻塞,並且該對象上的鎖被釋放。而調用任意對象的notify()方法則導致因調用該對象的 wait() 方法而阻塞的線程中隨機選擇的一個解除阻塞(但要等到獲得鎖後才真正可執行)。

                  有synchronized的地方不一定有wait,notify

                  有wait,notify的地方必有synchronized.這是因為wait和notify不是屬於線程類,而是每一個對象都具有的方法(事實上,這兩個方法是Object類裡的),而且,這兩個方法都和對象鎖有關,有鎖的地方,必有synchronized。

                  與ReentrantLock對比:

                  1.lock可以有多個條件,synchronized只有一個

                  2.synchronized不可中斷,lock可以,也能設置超時

                  3.lock需要手動釋放鎖

                  4.lcok在競爭激烈時,並發性能更好。synchronized在不激烈時,由於JVM優化,性能更好些。輕量鎖、偏向鎖、鎖消除等。


  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved