java併發編程學習1--基礎知識

【java內存模型簡介

JVM中存在一個主存區(Main Memory或Java Heap Memory),Java中全部變量都是存在主存中的,對於全部線程進行共享,而每一個線程又存在本身的工做內存(Working Memory),工做內存中保存的是主存中某些變量的拷貝,線程對全部變量的操做並不是發生在主存區,而是發生在工做內存中,而線程之間是不能直接相互訪問,變量在程序中的傳遞,是依賴主存來完成的。而在多核處理器下,大部分數據存儲在高速緩存中,若是高速緩存不通過內存的時候,也是不可見的一種表現。在Java程序中,內存自己是比較昂貴的資源,其實不只僅針對Java應用程序,對操做系統自己而言內存也屬於昂貴資源,Java程序在性能開銷過程當中有幾個比較典型的可控制的來源。synchronized和volatile關鍵字提供的內存中模型的可見性保證程序使用一個特殊的、存儲關卡(memory barrier)的指令,來刷新緩存,使緩存無效,刷新硬件的寫緩存而且延遲執行的傳遞過程,無疑該機制會對Java程序的性能產生必定的影響。java

【java線程的運行機制

在java虛擬機進程中,執行程序代碼的任務是由線程看來完成的。每一個線程都有一個獨立的程序計數器和方法調用棧。程序計數器:pc寄存器,當線程執行一個方法時,程序計數器指向方法區中下一條要執行的字節碼指令。方法調用棧:用來跟蹤線程運行中一系列方法的調用過程,棧中的元素稱爲棧幀。每當線程調用一個方法,就會壓棧一個新幀,幀用來保存方法的參數,局部變量,運算過程當中產生的臨時數據。java虛擬機的主線程是它從啓動類的main()方法開始運行。此外,用戶也能夠建立本身的線程,兩種方式:繼承 Thread 類,實現 Runnable 接口。
可是運行一個線程必須使用Thread.strat(),切記:1.不可直接運行run(),直接運行run()只是單純的方法調用,並不會產出新的線程。2.不要隨意覆蓋start(),若是必須覆蓋記得首先調用super.start()。線程是不會順序執行的,一切都由操做系統調度決定,而且一個線程只能啓動一次,第二次啓動會拋出:IllegalThreadStateException,可是並不會影響以前啓動的線程工做。編程

public class MyRunnable implements Runnable{
            @Override
            public void run() {
                    System.out.println("runnable running");
           }
    }

    
    public class MyThread extends Thread{
            @Override
             public void run(){
                System.out.println("thread running");
            }
    }

【java線程狀態

新建狀態:new 語句建立的狀態,此時它和其餘java對象同樣,僅僅在堆中被分配了內存。緩存

就緒狀態:當一個線程被其餘線程調用了start(),此時jvm會爲它建立程序計數器和方法調用棧。處於改狀態的線程位於可運行池,等待獲取CPU的執行權。數據結構

運行狀態:處於改狀態的線程佔用CPU,正在執行程序代碼。若是計算機只有一個單核CPU那麼永遠hi只有一個線程處於改狀態。只有處於就緒狀態的線程纔可能成爲運行狀態。多線程

阻塞狀態:線程由於某些緣由放棄了CPU暫停執行。此時線程放棄CPU的執行權,直到進入就緒狀態纔可能再次變爲運行狀態。阻塞狀態3中狀況:併發

  1. 對象等待池阻塞:線程執行了某個對象的wait(),線程被jvm放入這個對象的等待池之中。(用sleep()方法的過程當中,線程不會釋放對象鎖。而當調用wait()方法的時候,線程會放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象調用notify()方法後本線程才進入對象鎖定池準備。)
  2. 對象同步鎖阻塞:線程試圖獲取對象的同步鎖,若是同步鎖已經被其餘線程持有,jvm會把該線程放入對象鎖池中。
  3. 其餘阻塞狀態:當前線程執行sleep(),或者調用其它線程的join()(把指定的線程加入到當前線程,能夠將兩個交替執行的線程合併爲順序執行的線程。好比在線程B中調用了線程A的Join()方法,直到線程A執行完畢後,纔會繼續執行線程B。),或者發出了IO請求。

死亡狀態:線程退出run(),有多是正常執行完成,也有可能碰見異常退出。可是都不會對其餘線程形成影響。Thread類有isAlive()(新建與死亡狀態返回false,其他狀態返回true)判斷線程是否存活。jvm

【線程調度

一個單核CPU在一個時刻只能執行一個機器指令。線程只有經過得到CPU才能執行本身的程序代碼。所謂多線程的併發執行,其實從宏觀上來看:各個線程輪流得到CPU的使用權,分別執行各自的任務。jvm採用搶佔式調度模型,是指先讓高優先級線程得到CPU。若是優先級相同,隨機選擇一個執行。處於運行狀態的線程或一直執行,直到不得不放棄CPU,通常有以下緣由:ide

1. jvm讓其放棄CPU轉入就緒狀態。        
2. 線程因某些緣由進入阻塞狀態。       
3. 運行結束退出run()。

值得注意一點:java的線程優先級使用Thread.setPriority(int)設置,一般三個靜態常量選擇:Thread.MAX_PRIORITY(默認:10),Thread.MIN_PRIORITY(默認:1),Thread.NORM_PRIORITY(默認:5)。可是各個操做系統的線程優先級並不相同,因此爲了確保程序可以在不一樣平臺正常執行,咱們只是用這三個值,不會使用1-10中的其餘數字。經常使用方法:函數

  • Thread.sleep(long millis): 當前線程放棄CPU進入阻塞狀態,通過milli毫秒後恢復就緒狀態,不放棄對象鎖的持有。性能

  • Thread.yield(): 讓出CPU執行權進入就緒狀態,給另外一個擁有相同或者大於優先級的線程,若是沒知足條件的線程,則什麼都不作。

  • Thread.join(): 當前線程調用另外一個線程的join(),而且等待被調用線程執行完後再繼續執行。

  • Object.wait(): 當前線程必須擁有當前對象鎖。若是當前線程不是此鎖的擁有者,會拋出IllegalMonitorStateException異常。喚醒當前對象鎖的等待線程使用notify或notifyAll方法,也必須擁有相同的對象鎖,不然也會拋出IllegalMonitorStateException異常。waite()和notify()必須synchronized函數或synchronized block中進行調用。若是在non-synchronized函數或non-synchronized block中進行調用,雖然能編譯經過,但在運行時會發生IllegalMonitorStateException的異常。

  • Object.notify(): 執行該方法的線程隨機喚醒對象等待池中的一個線程,並將其裝入對象鎖池之中。

【線程的同步與併發

併發編程三個概念:

  1. 原子性:一個操做或者多個操做,要麼所有成功,要麼所有失敗。

  2. 可見性:當多個線程訪問同一變量時,一個線程修改了該變量的值,其餘線程能當即看到修改後的值。

  3. 有序性:程序執行的順序按照代碼的前後順序執行。(你覺得這是廢話?請了解指令重排序)。這三個特性中2,3能夠由volatile關鍵字保證(2.緩存一致性協議,3.禁止指令重排序),1只能由同步方式保證。

同步是解決資源共享的有效手段。當一個線程在操做共享變量的時候,其餘線程只能等待。只有當該線程執行完同步代碼塊後,其餘線程纔能有機會操做共享資源。一般有以下幾種同步方式:

  1. synchorized關鍵字: 修飾方法或者使用同步代碼塊。

  2. ReentrantLock重入鎖對象: 鎖住共享變量的操做。

  3. 使用併發數據結構對象:Atomic系列,Concurrent系列等。

    可是同步的操做,代價較大,咱們應該儘量減小同步操做,是的一個線程能儘快的釋放鎖,減小其餘線程執行的時間。因爲等待一個鎖的線程只有在得到了這把鎖以後,
    才能繼續執行因此讓持有鎖的線程及時釋放鎖的至關重要的。

    如下狀況線程釋放鎖:

    1. 執行完同步代碼塊。

    2. 執行同步代碼塊的過程當中,碰見異常,線程死亡,鎖被釋放。

    3. 執行同步代碼塊的過程當中,執行了鎖所屬對象的wait(),這個線程會釋放鎖進入對象等待池。

如下狀況線程不會釋放鎖:

  1. 執行同步代碼塊的過程當中,執行了Thread.sleep(),當前線程放棄CPU開始睡眠進入阻塞狀態,可是不會釋放鎖。

  2. 執行同步代碼塊的過程當中,執行了Thread.yield(),當前線程放棄CPU開始睡眠進入就緒狀態,可是不會釋放鎖。

  3. 執行同步代碼塊的過程當中,其餘線程執行了當前線程的suspend()(已廢棄,同時廢棄的還有:Thread.stop(),Thread.resume()),當前線程被暫停,可是不會釋放鎖。 死鎖兩個線程互相等待對方持有的鎖,通通進入阻塞狀態,jvm不檢測也不避免這種狀況。

【線程通訊

不一樣的線程須要協做完成工做(一種狀況是:線程2須要線程1的執行結果)。

- Object.wait(): 執行該放大的線程釋放它持有的該對象的共享鎖(前提時必須持有該共享鎖),該線程進入對象等待池,等待其餘線程將其喚醒。
    
- Object.notify(): 執行該方法的線程隨機喚醒對象等待池中的一個線程,並將其裝入對象鎖池之中。

補充:

1.鎖池:假設線程A已經擁有了某個對象(注意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),因爲這些線程在進入對象的synchronized方法以前必須先得到該對象的鎖的擁有權。可是該對象的鎖目前正被線程A擁有,因此這些線程就進入了該對象的鎖池中。

2.等待池:假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖(由於wait()方法必須出如今synchronized中,這樣天然在執行wait()方法以前線程A就已經擁有了該對象的鎖),同時線程A就進入到了該對象的等待池中。若是另外的一個線程調用了相同對象的notifyAll()方法,那麼處於該對象的等待池中的線程就會所有進入該對象的鎖池中,準備爭奪鎖的擁有權。若是另外的一個線程調用了相同對象的notify()方法,那麼僅僅有一個處於該對象的等待池中的線程(隨機)會進入該對象的鎖池。步驟以下(後面會有代碼實例):

1. t1執行s的一個同步代碼塊,t1持有s的共享鎖,t2在s的鎖池中等待。
   
   2. t1在同步代碼中執行s.wait(0,t1釋放s的共享鎖,進入s的等待池。
   
   3. s的鎖池中t2得到共享鎖執行s的另外一同步代碼塊。
   
   4. t2在同步代碼塊中執行s.notify(),JVM將t1從s的等待池轉入s的鎖池。
   
   5. t2完成同步代碼,釋放鎖,t1得到鎖繼續執行同步代碼。

eg:兩個線程,一個線程將某個對象的某個成員變量的值加1,而另一個線程將這個成員變量的值減1.使得該變量的值始終處於[0,2].初始值爲0:

【中斷阻塞

當一個線程處於阻塞狀態時,另外一個線程調用阻塞線程的interrupt(),阻塞線程收到InterruptException,並退出阻塞狀態,開始進行異常處理。代碼:

@Override            
        public void run() {                
            System.out.println("runnable running");                
            try {                   
                 Thread.sleep(1l);                
            } catch (InterruptedException e) {     
                  //-----start異常處理----                    
                e.printStackTrace();                   
                  //-----end異常處理-----                
            }            
        }

【總結

併發編程的知識很是複雜,以上只是一些皮毛,後續還將學習Synchronized,ReentrantLock,Future,FutureTask,Executor,Fork/Join,CompletableFuture,Map-Reduce等相關知識,最後用一個實際項目來完成這部分知識的學習。

相關文章
相關標籤/搜索