併發基礎知識

併發

Synchronized

1.synchronized關鍵字原理是什麼?

synchronized是JVM實現同步互斥的一種方式,被synchronized修飾的代碼塊在代碼編譯後在同步代碼塊開始時插入monitorenter 字節碼指令 ,在同步代碼塊結束和異常處會插入monitorexit指令。JVM會保證每一個monitorenter 都有monitorexit於之匹配,任何一個對象都有一個monitor對象和其關聯,當線程執行到monitorenter 指令的時候會嘗試獲取對象對應monitor的全部權,也就是對象的鎖,在代碼塊結束的或者異常的時候會釋放對象對應monitor的全部權。同步方法使用的是acc_synchronized指令實現的同步。html

2.synchronized是重入的嗎?

synchronized是重入的,當一個線程獲取執行到monitorenter 指令的時候會獲取對象對應的monitor的全部權,即得到鎖,鎖的計數器會+1。當這個線程在次獲取鎖的時候鎖的計數器會再+1。當同步代碼塊結束的時候會將鎖的計數器-1,直到鎖的計數器爲0,則釋放鎖的持有權。JVM以這種方式來實現synchronized的可重入性。java

3.synchronized是如何肯定鎖的對象的?

  • 同步代碼塊使用的是括號內的對象git

  • 同步方法使用的是當前的實例對象程序員

  • 靜態方法使用的當前類的class對象(全局鎖)github

4.synchronized有哪些優化?

在 Java 6 以前,Monitor 的實現徹底依賴底層操做系統的互斥鎖來實現 ,若是要將一個線程進行阻塞或喚起都須要操做系統的協助,這就須要從用戶態切換到內核態來執行,這種切換代價十分昂貴,很耗處理器時間,現代 JDK 中作了大量的優化。算法

  • 偏向鎖編程

    當一個線程訪問同步塊並得到鎖的時候會在對象頭的Mark Word和棧幀中記錄線程的ID,之後這個線程再獲取鎖的時候只須要比較下對象頭中存儲的偏向鎖的ID,不須要進行CAS操做加鎖和解鎖便可得到鎖的使用權。api

    若是測試失敗則須要檢查下對像頭Mark Word中的鎖標識是不是偏向鎖,若是是則嘗試使用CAS將對象頭中的偏向鎖指向當前線程,不然使用CAS競爭鎖。數組

  • 輕量級鎖bash

    偏向鎖運行在一個線程進入同步塊的狀況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級爲輕量級鎖,輕量級鎖的意圖是在沒有多線程競爭的狀況下,經過CAS操做嘗試將MarkWord更新爲指向LockRecord的指針 。

  • 重量級鎖

    虛擬機使用CAS操做嘗試將MarkWord更新爲指向LockRecord的指針,若是更新成功表示線程就擁有該對象的鎖;若是失敗,會檢查MarkWord是否指向當前線程的棧幀,若是是,表示當前線程已經擁有這個鎖;若是不是,說明這個鎖被其餘線程搶佔,此時膨脹爲重量級鎖。

5.synchronized是悲觀鎖嗎?什麼是CAS操做?

synchronized是悲觀鎖,它的同步策略是不管是否會有線程競爭都會加鎖。CAS操做是一種樂觀鎖的核心算法,在執行CAS操做的時候不會掛起線程,他的核心思想是內存值,舊的預期值,新值。在提交的時候比較內存值和舊的預期值是否相等,不相等經過一個while操做新計算想要修改的新值 (自旋)。

6.CAS操做和synchronized相比有什麼優勢和缺點?

  • synchronized涉及到了操做系統用戶模式和內核模式的切換,性能比CAS操做低。
  • CAS操做只能保證一個變量的原子更新,沒法保證多個變量的原子更新。
  • CAS長時間自選會致使CPU開銷增大。
  • CAS存在ABA問題,解決思路是增長版本號。

7.synchronized的內存語義是什麼?

獲取鎖會使該線程對應的本地內存置爲失效,線程直接從主內存獲取共享變量。 鎖的釋放會使該線程將本地內存中的共享變量寫入到住內存。

8.什麼是鎖消除和鎖粗化?

  • 鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,可是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除主要斷定依據來源於逃逸分析的數據支持

  • 鎖粗化,若是虛擬機探測到有這樣一串零碎的操做都對同一個對象加鎖,將會把加鎖同步的範圍擴展到整個操做序列的外部,這樣就只須要加鎖一次就夠了

ReentrantLock

1.ReentrantLock的實現原理是什麼?

sychronized是JVM原生的鎖,是經過對象頭設置標記實現的,ReentrantLock是基於Lock接口的實現類,經過AQS框架實現的一個可重入鎖。

  • AQS(AbstractQueuedSynchronizer 類)是一個用來構建鎖和同步器的框架,各類 Lock 包中的鎖(經常使用的有 ReentrantLock、ReadWriteLock),以及其餘如 Semaphore、CountDownLatch等都是經過AQS實現的。
  • 隊列同步器維護了一個基於雙向鏈表實現的同步隊列,當線程獲取同步狀態失敗的時候,會將當前線程維護爲一個節點存入到同步隊列的隊尾,而且阻塞當前的線程。而首節點是獲取到同步狀態的節點,當首節點釋放同步狀態的時候會喚醒後繼節點,後繼節點獲取同步狀態成功後會將本身設置爲首節點。
  • AQS獲取同步狀態的方式是在內部定義了一個volatile int state的變量用來表示同步狀態。當線程調用 lock 方法時 ,若是 state=0,說明沒有任何線程佔有共享資源的鎖,能夠得到鎖並將 state=1;若是 state=1,則說明有線程目前正在使用共享變量,線程必須加入同步隊列進行等待。
  • AQS 經過內部類 ConditionObject 構建等待隊列,等待隊列可能會有多個,當 Condition 調用 wait() 方法後,線程將會加入等待隊列中,而當 Condition 調用 signal() 方法後,線程將從等待隊列轉移動同步隊列中進行鎖競爭,AQS 和 Condition 各自維護了不一樣的隊列,在使用 Lock 和 Condition 的時候,其實就是兩個隊列的互相移動

2.ReentrantLock和sychronized使用上有什麼區別?

  • ReentrantLock在獲取鎖的時候能夠優先響應中斷。
  • ReentrantLock支持超時獲取鎖,若是超過超時時間則返回。
  • ReentrantLock能夠嘗試獲取鎖,若是鎖被其餘線程持有,則返回
  • ReentrantLock能夠實現公平鎖。

3.經常使用的同步器有哪些?

  • CountDownLancth
CountDownLatch(int count) #構造一個以給定計數CountDownLatch
await() #等待當前的計數器清零
await(long timeout, TimeUnit unit) #等待當前的計數器清零或到達超時時間
countDown() #減小鎖存器的計數,若是計數達到零,釋放全部等待的線程。
複製代碼

CountDownLantch使用案例:併發測試工具

  • CyclicBarrier
CyclicBarrier(int parties) #構造一個新的CyclicBarrier,攔截線程的數量是parties
CyclicBarrier(int parties, Runnable barrierAction) #構造一個新的CyclicBarrier,攔截線程的數量是parties,到達屏障時優先執行barrierAction
await() #等待
await(long timeout, TimeUnit unit) #帶超時時間等待
reset() #將屏障重置爲初始狀態。

複製代碼
  • Semaphore
Semaphore(int permits) #建立一個Semaphore與給定數量的許可證和非公平公平設置
Semaphore(int permits, boolean fair) #建立一個 Semaphore與給定數量的許可證和給定的公平設置。
acquire() #獲取許可
release() #釋放許可
複製代碼
  • Exchanger 線程間交換數據
exchange(V x) 
複製代碼

CountDownLatch 是不能夠重置的,因此沒法重用,CyclicBarrier 沒有這種限制,能夠重用。CountDownLatch 通常用於一個線程等待N個線程執行完以後,再執行某種操做。CyclicBarrier 用於N個線程互相等待都達到某個狀態,再執行。

4.ReadWriteLock 和 StampedLock有什麼區別?

ReentrantReadWriteLock 在沒有任何讀寫鎖時,才能夠取得寫入鎖,策略是悲觀讀。然而,若是讀取執行狀況不少,寫入不多的狀況下,使用 ReentrantReadWriteLock 可能會使寫入線程遭遇飢餓(Starvation)問題,也就是寫入線程吃吃沒法競爭到鎖定而一直處於等待狀態。StampedLock控制鎖有三種模式(寫,讀,樂觀讀 )一個StampedLock狀態是由版本和模式兩個部分組成,鎖獲取方法返回一個數字做爲票據stamp,它用相應的鎖狀態表示並控制訪問,數字0表示沒有寫鎖被受權訪問,在讀鎖上分爲悲觀鎖和樂觀鎖。若讀的操做不少,寫的操做不多的狀況下,你能夠樂觀地認爲,寫入與讀取同時發生概率不多,所以不悲觀地使用徹底的讀取鎖定 。

class Point {
   private double x, y;
   private final StampedLock sl = new StampedLock();
   void move(double deltaX, double deltaY) { // an exclusively locked method
     long stamp = sl.writeLock();
     try {
       x += deltaX;
       y += deltaY;
     } finally {
       sl.unlockWrite(stamp);
     }
   }
  //下面看看樂觀讀鎖案例
   double distanceFromOrigin() { // A read-only method
     long stamp = sl.tryOptimisticRead(); //得到一個樂觀讀鎖
     double currentX = x, currentY = y; //將兩個字段讀入本地局部變量
     if (!sl.validate(stamp)) { //檢查發出樂觀讀鎖後同時是否有其餘寫鎖發生?
        stamp = sl.readLock(); //若是沒有,咱們再次得到一個讀悲觀鎖
        try {
          currentX = x; // 將兩個字段讀入本地局部變量
          currentY = y; // 將兩個字段讀入本地局部變量
        } finally {
           sl.unlockRead(stamp);
        }
     }
     return Math.sqrt(currentX * currentX + currentY * currentY);
   }
//下面是悲觀讀鎖案例
   void moveIfAtOrigin(double newX, double newY) { // upgrade
     // Could instead start with optimistic, not read mode
     long stamp = sl.readLock();
     try {
       while (x == 0.0 && y == 0.0) { //循環,檢查當前狀態是否符合
         long ws = sl.tryConvertToWriteLock(stamp); //將讀鎖轉爲寫鎖
         if (ws != 0L) { //這是確認轉爲寫鎖是否成功
           stamp = ws; //若是成功 替換票據
           x = newX; //進行狀態改變
           y = newY; //進行狀態改變
           break;
         }
         else { //若是不能成功轉換爲寫鎖
           sl.unlockRead(stamp); //咱們顯式釋放讀鎖
           stamp = sl.writeLock(); //顯式直接進行寫鎖 而後再經過循環再試
         }
       }
     } finally {
       sl.unlock(stamp); //釋放讀鎖或寫鎖
     }
   }
 }
複製代碼

ReentrantReadWriteLock使用時要注意鎖的升級和降級問題: 讀寫鎖使用

JAVA 內存模型

1.通訊和同步

通訊 是指線程之間以何種機制來交換信息。在命令式編程中,線程之間的通訊機制有兩種:共享內存 和 消息傳遞。 在共享內存的併發模型裏,線程之間共享程序的公共狀態,線程之間經過寫-讀內存中的公共狀態隱式進行通訊。 在消息傳遞的併發模型裏,線程之間沒有公共狀態,線程之間必須經過明確的發送消息顯式進行通訊。 同步 是指程序用於控制不一樣線程之間操做發生相對順序的機制。 Java 的併發採用的是共享內存模型,Java 線程之間的通訊老是隱式進行,整個通訊過程對程序員徹底透明。

2.JAVA 內存模型的抽象(JMM)

在 Java 中,全部實例域、靜態域 和 數組元素存儲在堆內存中,堆內存在線程之間共享,   局部變量、方法定義參數 和異常處理器參數 不會在線程之間共享,它們不會有內存可見性問題,也不受內存模型的影響。

JMM 定義了線程與主內存之間的抽象關係: 共享變量都存儲在主內存,每條線程還有本身的工做內存,保存了被該線程使用到的變量的主內存副本拷貝。線程對變量的全部操做(讀取、賦值)都必須在工做內存中進行,不能直接讀寫主內存的變量。不一樣的線程之間也沒法直接訪問對方工做內存的變量,線程間變量值的傳遞須要經過主內存。

3.volatile關鍵字的內存語義

對一個 volatile 變量的讀,老是能看到(任意線程)對這個 volatile 變量最後的寫入。對一個 volatile 變量的讀會將本地內存的值置爲失效從主內存獲取。對一個 volatile 變量的寫會將本地內存的值寫入主內存。被volatile關鍵字修飾變量會靜止指令重排序優化。被volatile關鍵字修飾的變量只能保證單個變量的原子性相似於 volatile++ 這種複合操做,這些操做總體上不具備原子性。

4.final關鍵字內存語義

1)在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變量,這兩個操做之間不能重排序。 2)初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操做之間不能重排序。

寫final域的重排序規則能夠確保:在對象引用爲任意線程可見以前,對象的final域已經被正確初始化過了,而普通域不具備這個保障。

5.happens-before原則

在 JMM 中,若是一個操做執行的結果須要對另外一個操做可見,那麼這兩個操做之間必需要存在 happens-before 關係。這裏提到的兩個操做既能夠是在一個線程以內,也能夠是在不一樣線程之間。

  • 程序順序規則:一個線程中的每一個操做,happens-before 於該線程中的任意後續操做。
  • 監視器鎖規則:對一個監視器的解鎖,happens-before 於隨後對這個監視器的加鎖。
  • volatile 變量規則:對一個 volatile 域的寫,happens-before 於任意後續對這個 volatile 域的讀。
  • 傳遞性:若是 A happens-before B,且 B happens-before C,那麼 A happens-before C。 ###6.AS-IF-SERIAL原則 as-if-serial 語義的意思指:無論怎麼重排序(編譯器和處理器爲了提升並行度),(單線程)程序的執行結果不能被改變。

線程池

1.爲何要使用線程池?

  • 建立/銷燬線程伴隨着系統開銷,過於頻繁的建立/銷燬線程,會很大程度上影響處理效率 。

  • 線程併發數量過多,搶佔系統資源從而致使阻塞。

  • 對線程進行一些簡單的管理 。

2.線程池的核心參數

  • corePoolSize(線程池的基本大小):當提交一個任務到線程池時,線程池會建立一個線程來執行任務,即便其餘空閒的基本線程可以執行新任務也會建立線程,等到須要執行的任務數大於線程池基本大小時就再也不建立。若是調用了線程池的prestartAllCoreThreads()方法,線程池會提早建立並啓動全部基本線程。
  • maximumPoolSize(線程池最大數量):線程池容許建立的最大線程數。若是隊列滿了,而且已建立的線程數小於最大線程數,則線程池會再建立新的線程執行任務。值得注意的是,若是使用了無界的任務隊列這個參數就沒什麼效果
  • keepAliveTime(線程活動保持時間):線程池的工做線程空閒後,保持存活的時間。因此,若是任務不少,而且每一個任務執行的時間比較短,能夠調大時間,提升線程的利用率。默認狀況下keepAliveTime對大於corePoolSize小於maximumPoolSize的線程有效,若是設置allowCoreThreadTimeout=true(默認false)時,核心線程纔會超時關閉。
  • workQueue(任務隊列):用於保存等待執行的任務的阻塞隊列。
  • ThreadFactory :用於設置建立線程的工廠,能夠經過線程工廠給每一個建立出來的線程設置更有意義的名字。
  • RejectedExecutionHandler (飽和策略):當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採起一種策略處理提交的新任務。這個策略默認狀況下是AbortPolicy,表示沒法處理新任務時拋出異常。

​ 1)AbortPolicy:直接拋出異常。

​ 2) CallerRunsPolicy:只用調用者所在線程來運行任務。

​ 3) DiscardOldestPolicy:丟棄隊列裏最近的一個任務,並執行當前任務。

​ 4) DiscardPolicy:不處理,丟棄掉

3.線程池如何工做?

  • 若是當前運行的線程少於corePoolSize,則建立新線程來執行任務(注意,執行這一步驟須要獲取全局鎖)

  • 若是運行的線程等於或多於corePoolSize,則將任務加入BlockingQueue。

  • 若是沒法將任務加入BlockingQueue(隊列已滿),則建立新的線程來處理任務(注意,執行這一步驟須要獲取全局鎖)。

  • 若是建立新線程將使當前運行的線程超出maximumPoolSize,任務將被拒絕,並調用RejectedExecutionHandler.rejectedExecution()方法。

4.線程池如何提交線程?

  • execute():ExecutorService.execute 方法接收一個 Runable 實例,它用來執行一個任務。

  • submit():ExecutorService.submit() 方法返回的是 Future 對象。能夠用 isDone() 來查詢 Future 是否已經完成,當任務完成時,它具備一個結果,能夠調用 get() 來獲取結果。也能夠不用 isDone() 進行檢查就直接調用 get(),在這種狀況下,get() 將阻塞,直至結果準備就緒。

###5.JAVA中默認的線程池有哪些?

  • SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
複製代碼

SingleThreadExecutor線程池只有一個核心線程在工做,也就是至關於單線程串行執行全部任務。若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它。此線程池保證全部任務的執行順序按照任務的提交順序執行。

  • FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }
複製代碼

FixedThreadPool 是固定大小的線程池,只有核心線程。每次提交一個任務就建立一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,若是某個線程由於執行異常而結束,那麼線程池會補充一個新線程。

  • CachedThreadPool
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
複製代碼

CachedThreadPool 是無界線程池,若是線程池的大小超過了處理任務所須要的線程,那麼就會回收部分空閒(60 秒不執行任務)線程,當任務數增長時,此線程池又能夠智能的添加新線程來處理任務,適合執行任務時間短的異步任務。

  • ScheduledThreadPool
public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
複製代碼

ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor,用於處理定時任務。

scheduleAtFixedRate:該方法在initialDelay時長後第一次執行任務,之後每隔period時長,再次執行任務。注意,period是從任務開始執行算起的。開始執行任務後,定時器每隔period時長檢查該任務是否完成,若是完成則再次啓動任務,不然等該任務結束後纔再次啓動任務。

img

scheduleWithFixDelay:該方法在initialDelay時長後第一次執行任務,之後每當任務執行完成後,等待delay時長,再次執行任務。

这里写图片描述

##併發容器 ###1.ConcurrentHashMap 在多線程環境下,使用HashMap進行put操做時會致使鏈表成環。

2.ThreadLocal

3. DelayQueue

4.PriorityQueue

其餘

  • 使用TimeUnit代替Thread.sleep()
相關文章
相關標籤/搜索