to meet you Java多線程與併發

 

 

2:java

hotspot中對象在內存的佈局是分3部分 算法

  1. 對象頭
  2. 實例數據
  3. 對其填充

這裏主要講對象頭:通常而言synchronized使用的鎖對象是存儲在對象頭裏的,對象頭是由Mark Word和Class Metadata Address組成編程

 

mark word存儲自身運行時數據,是實現輕量級鎖和偏向鎖的關鍵,默認存儲對象的hasCode、分代年齡、鎖類型、鎖標誌位等信息。緩存

因爲對象頭的信息是與對象定義的數據沒有關係的額外存儲成本,因此考慮到jvm的空間效率,mark word 被設計出一個非固定的存儲結構,以便存儲更多有效的數據,它會根據對象自己的狀態複用本身的存儲空間(輕量級鎖和偏向鎖是java6後對synchronized優化後新增長的)安全

Monitor:每一個Java對象天生就自帶了一把看不見的鎖,它叫內部鎖或者Monitor鎖(監視器鎖)。上圖的重量級鎖的指針指向的就是Monitor的起始地址。多線程

每一個對象都存在一個Monitor與之關聯,對象與其Monitor之間的關係存在多種實現方式,如Monitor能夠和對象一塊兒建立銷燬、或當線程獲取對象鎖時自動生成,當線程獲取鎖時Monitor處於鎖定狀態。併發

Monitor是虛擬機源碼裏面用C++實現的框架

源碼解讀:_WaitSet 和_EntryList就是以前學的等待池和鎖池,_owner是指向持有Monitor對象的線程。當多個線程訪問同一個對象的同步代碼的時候,首先會進入到_EntryList集合裏面,當線程獲取到對象Monitor後就會進入到_object區域並把_owner設置成當前線程,同時Monitor裏面的_count會加一。當調用wait方法會釋放當前對象的Monitor,_owner恢復成null,_count減一,同時該線程實例進入_WaitSet集合中等待喚醒。若是當前線程執行完畢也會釋放Monitor鎖並復位對應變量的值。jvm

接下來是字節碼的分析:ide

package interview.thread;

/**
 * 字節碼分析synchronized
 * @Author: cctv
 * @Date: 2019/5/20 13:50
 */
public class SyncBlockAndMethod {
    public void syncsTask() {
        synchronized (this) {
            System.out.println("Hello");
        }
    }

    public synchronized void syncTask() {
        System.out.println("Hello Again");
    }
}

而後控制檯輸入 javac thread/SyncBlockAndMethod.java

而後反編譯 javap -verbose thread/SyncBlockAndMethod.class

先看看syncsTask方法裏的同步代碼塊

從字節碼中能夠看出 同步代碼塊 使用的是 monitorenter 和 monitorexit ,當執行monitorenter指令時當前線程講試圖獲取對象的鎖,當Monitor的count 爲0時將獲的monitor,並將count設置爲1表示取鎖成功。若是當前線程以前有這個monitor的持有權它能夠重入這個Monnitor。monitorexit指令會釋放monitor鎖並將計數器設爲0。爲了保證正常執行monitorenter 和 monitorexit 編譯器會自動生成一個異常處理器,該處理器能夠處理全部異常。主要保證異常結束時monitorexit(字節碼中多了個monitorexit指令的目的)釋放monitor鎖

ps:重入是從互斥鎖的設計上來講的,當一個線程試圖操做一個由其餘線程持有的對象鎖的臨界資源時,將會處於阻塞狀態,當一個線程再次請求本身持有對象鎖的臨界資源時,這種狀況屬於重入。就像以下狀況:hello2也是會輸出的,並不會鎖住。

再看看syncTask同步方法

解讀:這個字節碼中沒有monitorenter和monitorexit指令而且字節碼也比較短,其實方法級的同步是隱式實現的(無需字節碼來控制)ACC_SYNCHRONIZED是用來區分一個方法是否同步方法,若是設置了ACC_SYNCHRONIZED執行線程將持有monitor,而後執行方法,不管方法是否正常完成都會釋放調monitor,在方法執行期間,其餘線程都沒法在得到這個monitor。若是同步方法在執行期間拋出異常並且在方法內部沒法處理此異常,那麼這個monitor將會在異常拋到方法以外時自動釋放。

 

java6以前Synchronized效率低下的緣由:

在早期版本Synchronized屬於重量級鎖,性能低下,由於監視器鎖(monitor)是依賴於底層操做系統的的MutexLock實現的。

而操做系統切換線程時須要從用戶態轉換到核心態,時間較長,開銷較大

java6之後Synchronized性能獲得了很大提高(hotspot從jvm層面作了較大優化,減小重量級鎖的使用):

  1. Adaptive Spinning 自適應自旋
  2. Lock Eliminate 鎖消除
  3. Lock Coarsening 鎖粗化
  4. Lightweight Locking 輕量級鎖
  5. Biased Locking偏向鎖
  6. ……

自旋鎖:

  • 許多狀況下,共享數據的鎖定狀態持續時間較短,切換線程不值得
  • 經過讓線程執行while循環等待鎖的釋放,不讓出CPU
  • java4就引入了,不過默認是關閉的,java6後默認開啓的
  • 自旋本質和阻塞狀態並不相同,若是鎖佔用時間很是短,那自旋鎖性能會很好
  • 缺點:若鎖被其餘線程長時間佔用,會帶來許多性能上的開銷,由於自旋一直會佔用CPU資源且白白消耗掉CPU資源。
  • 若是線程超過了限定次數尚未獲取到鎖,就該使用傳統方式掛起線程(能夠設置VM的PreBlockSpin參數來更改限定次數)

 

 

自適應自旋鎖:(java6引入,jvm對鎖的預測會愈來愈精準,jvm也會愈來愈聰明)

  1. 自選次數再也不固定
  2. 由前一次在同一個鎖上的自旋時間及鎖擁有者的狀態來決定(若是在同一個鎖對象上自旋等待剛剛成功獲取過鎖而且持有鎖的線程正在運行中,jvm會認爲該鎖自旋獲取到鎖的可能性很大,會自動增長等待時間,相反jvm 若是可能性很小會省掉自旋過程,避免浪費)

鎖消除:jvm的另外一種鎖優化,更完全的優化

  • JIT編譯時,對運行上下文進行掃描,去除不可能存在的競爭的鎖,消除毫無心義的鎖

鎖粗化:另外一種極端,鎖消除的做用在儘可能小的範圍使用鎖,而鎖粗化則相反,擴大加鎖範圍。好比加鎖出如今循環體中,每次循環都要執行加鎖解鎖的,如此頻繁操做比較消耗性能

  • 擴大加鎖範圍,避免反覆的加鎖和解鎖

synchronized的四種狀態

  1. 無鎖
  2. 偏向鎖
  3. 輕量級鎖
  4. 重量級鎖

鎖膨脹方向:無鎖->偏向鎖->輕量級鎖->重量級鎖,synchronized會隨着競爭狀況逐漸升級,如出現了閒置的monitor也會出現鎖降級

偏向鎖:減小同一個線程獲取鎖的代價

  1. 大多數狀況下,鎖不存在多線程競爭,老是由同一個線程屢次得到

ps:核心思想就是若是一個線程得到了鎖,那麼鎖就進入偏向模式,此時MarkWord的結構也變成偏向鎖結構,當該線程再次請求鎖時,無需再作任何同步操做,即獲取鎖的過程只須要檢查MarkWord的鎖標記位爲偏向鎖以及當前線程ID等於MarkWord的ThreadID便可,這樣就省去了大量有關鎖申請的操做

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

輕量級鎖:

輕量級鎖是由偏向鎖升級而來的,偏向鎖運行再一個線程進入同步塊的狀況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級爲輕量級鎖

適用場景:線程交替執行的同步塊

若存在同一時間訪問同一鎖的狀況,就會致使輕量級鎖膨脹爲重量級鎖

輕量級鎖的加鎖過程:

此圖來自https://blog.csdn.net/zqz_zqz

  1. 在代碼進入同步塊的時候,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲「01」狀態,是否爲偏向鎖爲「0」),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間(線程私有的棧幀裏),用於存儲鎖對象目前的Mark Word的拷貝(對象是存在堆中的,因此對象的MarkWord也再堆中),官方稱之爲 Displaced Mark Word。
  2. 拷貝對象頭中的Mark Word複製到鎖記錄中;
  3. 拷貝成功後,虛擬機將使用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock record裏的owner指針指向object mark word。若是更新成功,則執行步驟4,不然執行步驟5。
  4. 若是這個更新動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位設置爲「00」,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如圖所示。
  5. 若是這個更新操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行。不然說明多個線程競爭鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲「10」,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。 而當前線程便嘗試使用自旋來獲取鎖,自旋就是爲了避免讓線程阻塞,而採用循環去獲取鎖的過程。 

鎖的內存語義

  • 當線程釋放鎖時,java內存模型會把該線程對應的本地內存中的共享變量刷新到主內存中
  • 當線程獲取鎖時,java內存模型會把該線程對應的本地內存置爲無效,從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量

總結:

 

問:synchronized和ReentrantLock的區別?

ReentrantLock(可重入鎖)

  • 位於java.util.concurrent.locks包(著名的juc包是由Doug lea大神寫的AQS抽象類框架衍生出來的應用)
  • 和CountDownLatch、FutureTask、Semaphore同樣基於AQS實現
  • 可以實現比synchronized更細粒度的控制,如控制fairness
  • 調用lock()後,必須調用unlock()釋放鎖
  • 性能未必比synchronized高,而且也是可重入的

ReentrantLock公平性設置

ReentrantLock fairLock = new ReentrantLock(true);

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

公平鎖:獲取鎖的順序按前後調用lock方法的順序(慎用,一般公平性沒有想象的那麼重要,java默認的調用策略不多會有飢餓狀況的發生,與此同時若要保證公平性,會增長額外的開銷,致使必定的吞吐量降低)

非公平鎖:獲取鎖的順序是無序的,synchronized是非公平鎖

例子:

package interview.thread;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Author: cctv
 * @Date: 2019/5/21 11:46
 */
public class ReentrantLockDemo implements Runnable {

    private static ReentrantLock lock = new ReentrantLock(false);

    @Override
    public void run() {
        while (true) {
            lock.lock();
            System.out.println(Thread.currentThread().getName() + " get lock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }

    }

    public static void main(String[] args) {
        ReentrantLockDemo rtld = new ReentrantLockDemo();
        Thread t1 = new Thread(rtld);
        Thread t2 = new Thread(rtld);
        t1.start();
        t2.start();
    }
}

公平鎖 new ReentrantLock(true);

非公平鎖 new ReentrantLock(false);

ReentrantLock將鎖對象化

  • 判斷是否有線程,或者某個特定線程再排隊等待獲取鎖
  • 帶超時的獲取鎖嘗試
  • 感知有沒有成功獲取鎖

是否能將wait\notify\notifyAll對象化

  • java.util.concurrent.locks.Condition

總結synchronized和ReentrantLock的區別:

  1. synchronized是關鍵字,ReentrantLock是類
  2. ReentrantLock能夠對獲取鎖的等待時間進行設置,避免死鎖
  3. ReentrantLock能夠獲取各類鎖信息
  4. ReentrantLock能夠靈活的實現多路通知
  5. 機制:synchronized操做MarkWord,ReentrantLock調用Unsafe類的park()方法

 

 

volatile和synchronized的區別

  • volatile本質是在告訴jvm當前變量在寄存器(工做內存)中的值是不肯定的,須要從主存中讀取; synchronized則是鎖定當前變量,只有當前線程能夠訪問該變量,其餘線程被阻塞住。
  • volatile僅能使用在變量級別;synchronized則可使用在變量、方法、和類級別的
  • volatile僅能實現變量的修改可見性,不能保證原子性;而synchronized則能夠保證變量的修改可見性和原子性
  • volatile不會形成線程的阻塞;synchronized可能會形成線程的阻塞。
  • volatile標記的變量不會被編譯器優化;synchronized標記的變量能夠被編譯器優化

3:CAS(Co'mpare  and Swap)

一種高效實現線程安全性的方法

一、支持原子更新操做、適用於計數器、序列發生器等場景。

二、屬於樂觀鎖機制,號稱 lock - free

三、CAS操做失敗時由開發者決定是繼續嘗試,仍是執行別的操做。

 

悲觀鎖:

 CAS 多數狀況下對開發者來講是透明的。

 

 

在使用CAS 前要考慮ABA 問題 是否影響程序併發的正確性,若是須要解決ABA 問題,改用傳統的互斥同步,可能會比原子性更高效。

 

java線程池,利用Exceutors建立不一樣的線程池知足不一樣場景需求:

  1. newSingleThreadExecutor() 建立一個單線程的線程池。這個線程池只有一個線程在工做,也就是至關於單線程串行執行全部任務。若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它。此線程池保證全部任務的執行順序按照任務的提交順序執行。
  2. newFixedThreadPool(int nThreads) 建立固定大小的線程池。每次提交一個任務就建立一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,若是某個線程由於執行異常而結束,那麼線程池會補充一個新線程。
  3.  newCachedThreadPool() 建立一個可緩存的線程池。若是線程池的大小超過了處理任務所須要的線程, 那麼就會回收部分空閒(默認60秒不執行任務)的線程,當任務數增長時,此線程池又能夠智能的添加新線程來處理任務。此線程池不會對線程池大小作限制,線程池大小徹底依賴於操做系統(或者說JVM)可以建立的最大線程大小
    1. 優勢就是靈活建立線程,因地制宜,任務少時很省資源。缺點就是可建立的線程上限太大,源代碼裏是Integer.MAX_VALUE大小,這個數量有點可怕
  4. newScheduledThreadPool() 建立一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。
  5. newWorkStealingPool() jdk8引入的,內部會構建ForkJoinPool,利用working-stealing算法,並行的處理任務,可是不保證處理順序

Fork/Join框架

  • 把大任務分割成若干個小任務並行執行,最終彙總每一個小任務結果後獲得大任務的框架
  • work-stealing算法:某個線程從其餘線程隊列裏竊取任務來執行

由於分割成若干個小任務由多個線程去執行,就會出現有的線程已經完成任務而有的還未完成任務,已經完成的線程就閒置了,爲了提高效率,讓已經完成任務的線程去其餘線程竊取隊列裏的任務來執行。爲了減小竊取線程對其餘線程的競爭,一般會使用雙端隊列,執行任務的線程從頭部拿任務執行,竊取線程是從隊列尾部拿任務執行

 

問:爲何要使用線程池?

  1. 下降資源消耗(經過重複利用已建立的線程來工做,下降建立線程和銷燬線程的消耗)
  2. 提升線程的可管理性(線程是稀缺資源,若是無限制的建立會不只會消耗系統資源還會下降系統的穩定性,使用線程池能夠統一的分配、調優、監控)

 

Executor的框架圖

JUC的三個Executor接口

  1. Executor:運行新任務的簡單接口,將人物提交和任務執行細節解耦
    1. 經過源碼看到newCachedThreadPool是返回的ExecutorService newSingleTreadExecutor是返回的FinalizableDelegatedExecutorService  最終都是繼承的Executor
    2. 接口Executor只有一個方法就是execute,對於不一樣的實現它多是建立一個新線程當即啓動,也多是使用已有的線程來運行傳入的任務,也多是根據線程池容量或阻塞隊列的容量來決定是否將傳入的任務放入阻塞隊列中或者拒絕接受任務
  2. ExecutorService:具有管理執行器和任務生命週期方法,提交任務機制更完善
    1. ExecutorService是Executor的擴展接口 提供了更方便的管理方法 最經常使用的是 shutdown submit
    2. submit參數有Callable、Runnable兩種 並返回Future
  3. ScheduledExecutorService:支持Future和按期執行任務

ThreadPoolExecutor的構造函數

  1. corePoolSize:核心線程數量
  2. maximunPoolSize:線程不夠用時可以建立的最大線程數
  3. workQueue:任務等待隊列(當前線程數量大於等於corePoolSize的時候,將任務封裝成work放入workQueue中。不一樣的隊列排隊機制不一樣)
  4. keepAliveTime:線程池維護線程的空閒時間,線程空閒超過這個時間就會被銷燬
  5. threadFactory:建立新線程,默認使用Executors.defaultThreadFactory(),新建立的線程是同樣的優先級、非守護線程

ps:newCachedThreadPool傳入的隊列是容量爲0的SynchronousQueue,(Java 6的併發編程包中的SynchronousQueue是一個沒有數據緩衝的BlockingQueue,生產者線程對其的插入操做put必須等待消費者的移除操做take,反過來也同樣)

handler:線程池的飽和策略

  1. AbortPolicy:直接拋出異常,這是默認策略
  2. CallerRunsPolicy: 用調用者所在多線程來執行任務
  3. DiscardOldestPolicy:丟棄隊列中最靠前的任務,並執行當前任務
  4. DiscardPolicy:直接丟棄任務
  5. 實現RejectedExecutionHandler接口自定義handler處理

execute方法執行流程以下:

線程池的狀態:

  1. RUNNING:可以接受新任務,而且也能處理阻塞隊列中的任務
  2. SHUTDOWN:不能接受新任務,但能夠處理存量任務
  3. STOP:再也不接受新任務,也不處理存量任務
  4. TIDYING:全部任務都已終止,正在進行最後的打掃工做,有效線程數爲0
  5. TERMINATED:terminated()方法執行完成後進入該狀態(該方法什麼也不作只是標識)

狀態轉換圖:

工做線程的生命週期:

問:如何選擇線程池大小?(沒有絕對的算法或規定,是靠經驗累計總結出來的)

  • CPU密集型:線程數=按照核數或者核數+1(由於若是線程太多會致使過多的上下文切換,致使沒必要要的開銷)
  • I/O密集型:線程數量=CPU核數*(1+平均等待時間/平均工做時間)
  • ps:

    阿里編碼規範指出:線程池不容許使用Executors去建立,而是經過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同窗更加明確線程池的運行規則,規避資源耗盡的風險。 說明:Executors各個方法的弊端:
    1)newFixedThreadPool和newSingleThreadExecutor:
      主要問題是堆積的請求處理隊列可能會耗費很是大的內存,甚至OOM。
    2)newCachedThreadPool和newScheduledThreadPool:
      主要問題是線程數最大數是Integer.MAX_VALUE,可能會建立數量很是多的線程,甚至OOM。

    例子:使用Guava的ThreadFactoryBuilder 

    輸出:

    不加劇試的輸出是:

    從例子中看出 maxPoolSize + QueueSize < taskNum 就會拋出拒絕異常 若是不catch這個異常程序沒法結束(這裏重試機制只是個demo,正確的作法是實現RejectedExecutionHandler接口自定義handler處理)

相關文章
相關標籤/搜索