併發學習小隨筆

進程和線程概念,區別?

每一個程序就是一個進程,進程獨佔內存空間,保存各自的運行狀態,相互間互不干擾且可以進行切換,爲併發處理任務提供了可能;(單核時只能有一個進程在執行);

一個進程可以包括多個線程(至少包含一個線程),線程是進程的執行單元;

區別:

  1. 進程是資源分配的最小單位,線程是CPU調度的最小單位;
  2. 進程有獨立的地址空間,互不影響;線程沒有獨立的地址空間;
  3. 進程切換的開銷比線程切換開銷大;

Tips:Java採用單線程模型,JVM虛擬機是多線程的(例如垃圾收集器線程等等);

Thread中的start和run方法區別?

區別:

start()方法源碼中會調用一個native的start0()方法(.cpp),會調用JVM_StartThread來創建並啓動子線程;

執行start()方法會創建一個新的子線程並啓動;執行run()方法只是Thread的一個普通方法調用;

Thread和Runnable區別?

區別:

  1. Thread是一個實現了Runnable接口的類並且實現了run方法;

    Thread的構造方法參數:Runnable實現類實例,FutrueTask實例

  2. 由於Java是單繼承的,推薦使用Runnable接口;

如何實現處理線程的返回值?

  1. 主線程等待法,主線程調用sleep方法(讓主線程去循環等待子線程結束並賦值;缺點:代碼臃腫,等待時間無法精準控制;);

  2. 使用Thread類的join()阻塞當前線程以等待子線程處理結束(依舊不是很精準);

  3. 通過Callable接口實現call()獲取線程返回值(通過FutrueTask Or 線程池獲取)

    FutrueTask的構造方法可以傳入Callable實現類的實例;

    isDone()可以判斷call是否執行結束;

    get()用來獲取線程完成後的返回值;

線程的六個狀態?

  1. 新建(new):創建線程後尚未啓動的線程(未調用start方法)

  2. 運行(Runnable):包含Running(調用start方法後)和Ready(位於線程池中,等待獲得cpu資源後就會變爲Running)

  3. 無限期等待(Waiting):不會被分配CPU執行時間,需要顯式喚醒;(調用沒有設置timeout參數的Object.wait方法;調用沒有設置timeout參數的Thread.join方法;LockSupport.park方法;喚醒需要notify或者notifyAll)

  4. 限期等待(Timed Waiting):在一定時間後會有系統自動喚醒;

    (調用Thred.sleep方法;調用設置timeout參數的Object.wait方法;調用設置timeout參數的Thread.join方法;LockSupport.parkNanos方法;LockSupport.parkUntil方法)

  5. 阻塞狀態(Blocked):等待鎖(進入Synchrionized代碼塊或者方法)

  6. 結束(Terminated):已終止的線程狀態,線程已經執行結束(終止後不能復活)

sleep和wait的區別?

  1. sleep是Thread類的方法;wait是Object的方法;

  2. sleep方法可以在任何地方使用;wait只能在synchronized代碼塊或者synchronized方法中使用(是因爲只有獲取到鎖才能釋放鎖);

  3. sleep只會讓出cpu,不會釋放鎖;wait會讓出cpu,還會釋放鎖;

notify與notifyAll區別 ?

  1. notifyAll會喚醒所有在等待池中的線程並添加到鎖池中去競爭鎖;(使用wait方法後的線程會釋放鎖進入等待池,不會去競爭鎖;如果使用notifyAll方法後,等待池中的線程會重新進入鎖池;當線程A獲得鎖,其他線程就會進入鎖池去等待鎖釋放進行競爭;)
  2. notify會隨機喚醒一個等待池中的線程添加到鎖池中去競爭鎖;

yield方法介紹?

  1. 當調用Thread.yield函數時,會給線程調度器一個暗示:當前線程願意讓出CPU使用,但是線程調度器有可能會忽略;
  2. yield不會對鎖行爲有影響(不會釋放鎖);

interrupt如何中斷線程?

stop()、suspend()、resume()已經被拋棄,突然停止線程並且釋放鎖;

目前使用的方法是interrupt(),通知線程應該中斷了;

  1. 如果線程是被阻塞狀態,那麼線程將直接退出被阻塞狀態,並且拋出InterruptedException異常;
  2. 如果線程是正常運行狀態,那麼會將該線程的中斷標記設置爲true,該線程將繼續正常運行,也不會突然停止;

也就是說interrupt不能真正的中斷線程,需要被調用的線程配合完成中斷;

  1. 在線程正常運行任務時,經常去查看本線程的中斷標誌位,如果中斷標誌位爲true,就應該自行停止線程;
  2. 如果線程處於正常活動狀態,那麼線程中斷標記被設置爲true,該線程會繼續正常運行;

線程狀態轉換

在這裏插入圖片描述

synchronized使用?

特性:互斥性(同一時間只能允許一個線程操作鎖)和可見性(在鎖被釋放前對共享變量所做的操作是對隨後獲得該鎖的線程是可見的);

分類:對象鎖與類鎖(類鎖和對象鎖是不干擾的)

  1. 對象鎖(同一個對象的同步代碼塊和同步方法是互斥的)

    1. 
    synchronized(this){
    	//同步代碼塊
    }
    2.
    synchronized  method(){
    	//同步方法體
    }
    
  2. 類鎖

    1.
    synchronized(.class){
    	//同步代碼塊
    }
    2.
    synchronized static method(){
    	//同步方法體
    }
    

synchronized基礎(Java對象頭和Monitor)?

  1. synchronized的鎖對象是存儲在對象頭中的,由MarkWord(存儲對象的運行時數據)和ClassMetadataAddress(jvm通過該指針確定這個對象是哪個類的數據)組成;

  2. Monitor(C++實現)

    底層ObjectMonitor.hpp:

    其中會有Entrylist(鎖池)、Waitlist(等待池)、Owner和Count,如果有多個線程訪問同步代碼時,會先進入Entrylist,線程獲得Monitor鎖後會進入Owner區域中,將Owner設置爲當前線程(初始爲null),Count+1;如果線程調用wait方法 ,將會進入Waitlist中,Owner恢復爲null,Count-1;

Tips:早期的synchronized鎖屬於重量級鎖,性能較低,線程切換需要依賴底層操作系統,需要從用戶態轉換到核心態,開銷太大;Java6後對synchronized鎖進行優化;

自旋鎖

  1. 很多情況下,共享數據的鎖定狀態持續時間較短,切換線程不值得;
  2. 通過讓線程執行循環來等待鎖的釋放,不會讓出cpu;
  3. 缺點:如果其他線程佔用鎖時間較長,會帶來許多額外的性能開銷;

Java6以後synchronized鎖優化?

  1. 自適應自旋鎖

    1. 自旋次數不再固定;
    2. 由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定(以上次自旋結果預測是否能獲得鎖);
  2. 鎖消除

    當jvm編譯時,對運行上下文進行掃描,去掉不可能存在競爭的鎖;

    //StringBuffer是線程安全的,以下sb只會在append方法中使用,不會被其他線程調用
    //jvm會自動清除內部的synchronized鎖
    StringBuffer sb = new StringBuffer();
    sb.append(str1).append(str2);
    
    
  3. 鎖粗化

    通過擴大加鎖的範圍,避免反覆加鎖和解鎖;

    StringBuffer sb = new StringBuffer();
    while(i<100){
        //連續的append會多次加鎖和解鎖
        //jvm會執行鎖粗化,將synchronized範圍擴大至只需要加一次鎖
        sb.apped(i);
    }
    
  4. 偏向鎖(減少同一線程獲取鎖的代價,適合只有一個線程訪問同步塊或同步方法)

    如果一個線程獲得了鎖,那麼鎖就進入偏向模式,MarkWord會將該鎖標記爲偏向鎖;當再次請求該鎖時,無需再做任何同步操作,只需要檢查MarkWord中標記是否爲偏向鎖,以及當前線程ID是否等於MarkWord中的ThreadID,省去了CAS加鎖和解鎖操作。

    缺點:不適用於鎖競爭比較激烈的多線程場合;

  5. 輕量級鎖(適用於線程交替執行同步塊或同步方法)

    是偏向鎖升級來的,使用自旋操作,競爭的線程不會阻塞,提高響應速度;

    缺點:若長時間得不到鎖,自旋會消耗CPU性能;

  6. 重量級鎖(適用於追求吞吐量,同步塊或同步方法使用執行時間較長)

    線程競爭不會使用自旋,不會消耗CPU;

    缺點:線程阻塞,響應時間慢,多線程下頻繁加鎖解鎖,性能消耗巨大;

synchronized的四種狀態(隨着線程競爭情況逐漸升級)

鎖膨脹方向(鎖升級):無鎖—>偏向鎖—>輕量級鎖—>重量級鎖

ReentrantLock(再入鎖)

  1. 位於JUC包下;和FutureTask都是基於AQS(隊列同步器)實現的;

  2. 可以實現比synchronized更細粒度的控制;例如fairness

  3. 調用lock()之後,必須調用unlock();(try調用lock(),finally調用unlock();)

  4. 與synchronized一樣是可重入的,當一個線程獲取已經獲取的鎖時,就會自動獲取成功;

ReentranLock公平性設置

ReentranLock fairLock = new ReentranLock(true);

參數爲true時,傾向於將鎖賦予等待時間最久的線程;

公平鎖:獲取鎖的順序安先後調用lock方法的順序;

非公平鎖:線程搶佔順序不一定;例如synchronized是非公平鎖;

ReentranLock與synchronized區別?

  1. synchronized是關鍵字;ReentranLock是類;
  2. ReentranLock可以對獲取鎖的等待時間進行設置,避免死鎖;
  3. ReentranLock可以獲取各種鎖的信息;
  4. ReentranLock可以靈活地實現多路通知;
  5. synchronized操作的是MarkWord,lock調用的是Unsafe類的park方法;

Java內存模型(JMM)

本身是抽象概念,並不是真實存在的,它描述的是一組規則或者規範,通過這組規範定義了程序中各個變量的訪問方式。

主內存:

  1. 存儲Java實例對象;
  2. 成員變量、類信息、常量、靜態變量等;
  3. 屬於數據共享的區域,多線程併發操作時會引發線程安全問題;

工作內存:

  1. 存儲當前方法的所有本地變量信息,本地變量對其他線程不可見;
  2. 字節碼行號指示器,Native方法信息;
  3. 屬於線程私有數據區域,不存在線程安全問題;

JMM中主內存和工作內存數據存儲類型以及操作方式

  1. 方法裏的基本數據類型將直接存儲在工作內存的棧幀結構中
  2. 引用類型的本地變量:引用存儲在工作內存中;實例存儲在主內存中;
  3. 成員變量、static變量、類信息均會存儲在主內存中;
  4. 主內存共享方式就是線程各自拷貝一份數據到工作內存,操作完成後刷新回主內存;

volatile(JVM提供的輕量級同步機制)

  1. 保證被volatile修飾的共享變量對所有線程總是可見的;
  2. 禁止指令重排;
  3. 保證可見性,不保證原子性;
  4. 性能略高於synchronized;

volatile如何實現立即可見?

當寫一個volatile變量時,JMM會把這個線程對應的工作內存刷新到主內存中;

當讀一個volatile變量時,JMM會把該線程的工作內存無效化,只能從主內存重新獲取最新的值;

volatile如何禁止指令重排?

內存屏障(CPU指令)

  1. 保證特定操作的執行順序;
  2. 保證某些變量的內存可見性;

通過插入內存屏障指令,禁止對內存屏障前後的指令進行重排序優化;

強制刷出各種CPU的緩存數據,因此CPU緩存也能讀取到數據的最新版本,保證可見性;

volatile與synchronized區別?

  1. volatile本質是告訴JVM當前變量在工作內存中是不準確的,需要從主內存中讀取;synchronized則是鎖定當前變量,只有當前線程可以訪問,其他線程進入鎖池等待;
  2. volatile只能使用在變量級別;synchronized可以使用在變量、方法、代碼塊以及類級別;
  3. volatile保證變量的修改可見性,不保證原子性;synchronized可以保證變量的修改可見性和原子性;
  4. volatile不會造成線程阻塞;synchronized可能會造成線程阻塞;
  5. volatile標記的變量不會被編譯器優化;synchronized可能會被編譯器優化;

CAS解釋

操作流程:先去主內存拿到要操作的數據到工作內存,經過計算後,刷新回主內存;

  1. 支持原子更新操作,適用於計數器,序列發生器等場景;

  2. 樂觀鎖機制;

  3. CAS操作失敗時可以決定是繼續嘗試,還是執行別的操作;
    在這裏插入圖片描述
    缺點:

  4. 若循環時間長,則開銷很大;

  5. 只能保證一個共享變量的原子操作;

  6. ABA問題;(如果一個數據從A->B->A,CAS則認爲從來沒有改變過;解決:JUC下的AtomicStampedReference);

synchronized,ReentranLock是悲觀鎖機制(始終假定會發生併發衝突),CAS(Compare and Swap)是樂觀鎖機制(始終假定不會發生併發衝突);

線程池(Executors)

  1. newFixedThreadPool(int nThreads)

    指定工作線程數量的線程池

  2. newCacheThreadPool()

    創建一個具有緩存功能的線程池

    1. 創建線程數量默認爲Integer.MAX_VALUE;
    2. 如果線程空閒超過指定時間,則會被終止並移出緩存;
    3. 系統長時間閒置,不會消耗太多資源;
  3. newSingleThreadExecutor()

    創建一個單線程工作的線程池

    1. 如果線程異常結束,會有另外一個線程來取代它;
    2. 保證每個線程的順序執行;
    3. 同一時間內不會有多個活動線程;
  4. newScheduledThreadPool(int corePoolSize)

    創建一個週期性執行線程的線程池,傳入核心線程數量;

  5. newSingleThreadScheduledExecutor()

    創建一個週期性執行線程單線程線程池

  6. newWorkStealingPool()(Java8加入)

    內部會構建ForkJoinPool,利用working-stealing算法(如果有線程已經執行結束,爲了提高利用率,將會竊取其他等待線程來執行;),可以並行處理任務,不保證處理順序;

  7. 使用ThreadPoolExecutor構造方法創建自定義線程池

在這裏插入圖片描述

線程池創建方法

		//指定線程數量的線程池
        ExecutorService pool1 = Executors.newFixedThreadPool(1);
        //緩存線程池
        ExecutorService pool2 = Executors.newCachedThreadPool();
        //單線程線程池
        ExecutorService pool3 = Executors.newSingleThreadExecutor();
        //週期性線程池
        ExecutorService pool4 = Executors.newScheduledThreadPool(1);
        //週期性單線程線程池
        ExecutorService pool5 = Executors.newSingleThreadScheduledExecutor();
        //Fork/Join線程池
        ExecutorService pool6 = Executors.newWorkStealingPool();
		//自定義線程池
ExecutorService threadPool = new ThreadPoolExecutor(
    3, 6, 0L, TimeUnit.MILLISECONDS, 
 new LinkedBlockingQueue<Runnable>());

線程池啓動方式

//創建線程池
ExecutorService executorService = Executors.newCachedThreadPool();
//創建線程
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("創建線程");
            }
        };
//提交線程並啓動
        executorService.submit(runnable);
//啓動線程
        executorService.execute(runnable);

爲什麼使用線程池(線程池優點)?

  1. 降低資源消耗(重複利用創建的線程,避免重複創建和銷燬線程的消耗);
  2. 提高線程的可管理性(統一分配,調優等);
  3. 提高響應速度(任務不需要等待線程創建就可以執行);

線程池運行流程

在這裏插入圖片描述
當提交線程之後,線程會進入工作隊列WorkQueue隊列,然後提交給內部線程池創建Worker開始工作,Worker管理線程的創建和銷燬;

在這裏插入圖片描述

線程池拒絕策略處理(ThreadPoolExecutor構造函數參數handler)

在這裏插入圖片描述
在這裏插入圖片描述

線程池狀態

  1. RUNNING:能接受新提交的任務,並且可以處理隊列中的任務;
  2. SHUTDOWN:不再接受新任務提交,可以繼續處理剩餘的任務;
  3. STOP:不再接受新任務提交,也不出來剩餘任務;
  4. TIDYING:所有的任務都已經終止;
  5. TERMINATED:terminated()方法執行完後進入該狀態,消亡;
    在這裏插入圖片描述

線程池大小如何確定?

CPU密集型:線程數=CPU核數+1;

I/O密集型:線程數=CPU核數*(1+平均等待時間/平均工作時間);

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章