【JAVA】【面試】【基礎篇】- 線程、鎖

再快不能快基礎,再爛不能爛語言! java

【基礎篇】- 線程

線程:一個程序同時執行多個任務,一般,每個任務稱爲一個線程。
串行: 對於單條線程執行多個任務,例以下載多個任務,須要下載完一個再下載另外一個。
並行:下載多個文件,開啓多條線程,多個文件同時下載。面試

  • 建立線程的方式及實現

    1. 繼承Thread類建立線程數據庫

    (1) 定義Thread類的子類,並重寫run方法,該run方法的方法體就表明了線程要完成的任務。所以把run()方法稱爲執行體。
      (2) 建立Thread子類的實例,即建立了線程對象。
      (3) 調用線程對象那個的start()方法來啓動該線程。
    
      知識點:
      Thread.currentThread()方法返回當前正在執行的線程對象。
      GetName()方法返回調用該方法的線程的名字。
    複製代碼

    2. 實現Runnable接口建立線程編程

    (1) 定義runnable接口的實現類,並重寫接口的run()方法,該run()方法的方法體一樣是該線程的線程執行體。
      (2) 建立Runnable實現類的實例,並以此實例做爲Thread的target來建立Thread對象,該Thread對象纔是真正的線程對象。
      (3) 調用線程對象的start()方法來啓動該線程。
    複製代碼

    3. 經過Callable和Future建立線程數組

    (1) 建立Callable接口的實現類,並實現call()方法,該call()方法做爲線程執行體,而且有返回值。
      (2) 建立Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值。(FutureTask是一個包裝器,它經過接收Callable來建立,它同時實現了Future和Runable接口。)
      (3) 使用FutureTask對象做爲Thread對象的Target建立並啓動新線程。
      (4) 調用FutureTask對象的get()方法來得到子線程執行結束後的返回值。
    複製代碼
  • 三種建立線程方法的對比

    (1)採用Runnable、Callable接口的方法建立多線程
      優點:線程類只是實現了Runnable接口或Callable接口,還能夠繼承其餘類。在這種方式下,
      多個線程能夠共享同一個target對象,因此很是適合多個相同線程來處理同一份資源的狀況,從而能夠將CPU,代碼,數據分開,造成清晰的模型,
      較好地體現了面對對象的思想。
      劣勢:編程稍微複雜,若是要訪問當前線程,則必須使用Thread.currentThread()方法。
      
      (2)使用繼承Thread類的方式建立多線程
      優點:編寫簡單,若是須要訪問當前線程,則無需使用Thread.currentThread()方法,直接使用this便可得到當前線程。
      劣勢: 線程類已經繼承了Thread類,因此不能再繼承其餘父類。
      
      (3) Runable和Callable的區別
      Callable規定(從新)的方法是call(),Runnale規定(重寫)的方法是run()。
      Callable的任務執行後可返回值,而Runnable的任務是不能返回值的。
      call方法能夠拋出異常,run方法不能夠。
      運行Callable任務能夠拿到一個Future對象,表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並檢索計算的結果。
      經過Future對象能夠了解任務執行狀況,可取消任務的執行,還可獲取執行結果。
    複製代碼
  • sleep() 、join()、yield()有什麼區別

    • sleep():緩存

      sleep()方法須要指定等待時間,它可讓當前正在執行的線程在指定的時間內暫停執行,進入阻塞狀態,該方法既可讓其餘同優先級或高優先級的線程獲得執行的機會,也可讓低優先級的線程獲得執行的機會。可是sleep()方法不會釋放"鎖標誌",也就是說若是有synchronized同步塊,其餘線程仍然不能訪問共享數據。安全

    • wait():bash

      wait()方法須要跟notify()以及notifyAll()兩個方法一塊兒介紹,這三個方法用於協調多個線程對共享數據的存取,因此必須在synchronized語句塊內使用,也就是說,notify()和notifyAll()的任務在調用這些方法前必須擁有對象的鎖。注意,它們都是Object類的方法,而不是Thread類的方法。數據結構

      wait()方法與sleep()方法的不一樣之處在於,wait()方法會釋放對象的"鎖屬性"。當調用某一對象的wait()方法後,會使當前線程暫停執行,並將當前線程放入對象等待池中,直到調用notify()方法後,將從對象等待池中移出任意一個線程並放入鎖標誌等待池中,只有鎖標誌等待池中的線程能夠獲取鎖標誌,它們隨時準備爭奪鎖的擁有權。當調用了某個對象的notifyAll()方法,會將對象等待池中的全部線程都移動到該對象的鎖等待池。多線程

      除了使用notify()和notifyAll()方法,還可使用帶毫秒參數的wait(longtimeout)方法,效果是在延遲timeout毫秒後,被暫停的線程將恢復到鎖標誌等待池。

      此外,wait(),notify()及notifyAll()只能在synchronized語句中使用,可是若是使用的是ReenTrantLock實現同步,解決方法是使用ReenTrantLock.newCondition()獲取一個Condition類對象,而後Condition的await(),signal()以及signalAll()分別對應上面的三個方法。

    • yield():

      yield()方法和sleep()方法相似,也不會釋放「鎖標誌」,區別在於,它沒有參數,即yield()方法只是使當前線程從新回到可執行狀態,因此執行yield()的線程有可能在進入到可執行狀態後立刻又被執行,另外yield()方法只能使用同優先級或者高優先級的線程獲得執行機會,這也和sleep()方法不一樣。

    • join():

      join()方法會使當前線程等待join()方法的線程結束後才能繼續執行。

  • 說說 CountDownLatch 原理

    CountDownLatch簡介:

    閉鎖是一種同步工具類,能夠延遲線程的進度直到其到達終止狀態。閉鎖的做用至關於一扇門,在閉鎖到達結束狀態以前,這扇門一直是關閉的,而且沒有任何線程能經過,當到達技術狀態時,這扇門會打開並容許全部的線程經過。當閉鎖到達結束狀態後,將不會再改變狀態,所以這扇門將永遠保持打開狀態。閉鎖能夠用來確保某些活動直到其餘活動都完成後才繼續執行。

    CountDownLatch是一種靈活的閉鎖實現,它是一個同步輔助類,在完成一組正在其餘線程中執行的操做以前,它容許一個或多個線程一直等待。

    CountDownLatch實現原理:

    原文連接:blog.csdn.net/qq_39241239…

CountDownLatch是經過「共享鎖」實現的。在建立CountDownLatch中時,會傳遞一個int類型參數count,該參數是「鎖計數器」的初始狀態,
表示該「共享鎖」最多能被count個線程同時獲取。當某線程調用該CountDownLatch對象的await()方法時,該線程會等待「共享鎖」可用時,
才能獲取「共享鎖」進而繼續運行,而「共享鎖」可用的條件,就是「鎖計數器」的值爲0!而「鎖計數器」的初始值爲count,每當一個線程調用
該CountDownLatch對象的CountDown()方法時,纔將「鎖計數器」-1;經過這種方式,必須有count個線程調用countDown()以後,
「鎖計數器」才爲0,而前面提到的等待線程才能繼續運行!
複製代碼
  • 說說 CyclicBarrier 原理

    CyclicBarrier簡介:

    柵欄相似於閉鎖,它能阻塞一線程直到某個事件發送。柵欄與閉鎖的關鍵區別在於,全部線程必須同時到達柵欄位置,才能繼續執行。閉鎖用於等待時間,而柵欄用於等待其餘線程。

    全部線程相互等待,直到全部的線程到達某一點時纔打開柵欄,而後線程能夠繼續執行

    CyclicBarrier實現原理:

    原文連接:blog.csdn.net/qq_39241239…

CyclicBarrier的源碼實現和CountDownLatch大相庭徑,CyclicBarrier基於Condition來實現的。CyclicBarrier類的內部有一個計數器,
每一個線程在到達屏障點的時候都會調用await方法將本身阻塞,此事計數器會減1,當計數器爲0的時候全部因調用await方法而被阻塞的線程將被喚醒。
複製代碼
  • 說說 CountDownLatch 與 CyclicBarrier 區別

    1. 這兩個類均可以實現一組線程在到達某個條件以前進行等待,它們內部都有一個計數器,當計數器的值不斷的減爲0的時候全部阻塞的線程將會被喚醒
    2. CountDownLatch的計數器是有使用者來控制的,調用await方法只是將本身阻塞而不會減小計數器的值。
      CyclicBarrier的計數器是由本身控制,調用await方法不只會將本身阻塞還會將減小計數器的值。
    3. CountDownLatch只能攔截一輪 CyclicBarrier能夠實現循環攔截(CyclicBarrier能夠實現CountDownLatch的功能,反之則不能)
    4. CountDownLatch的做用是容許1或N個線程等待其餘線程完成執行;
      CyclicBarrier則是容許N個線程相互等待。
    5. CountDownLactch的計數器沒法被重置;
      CyclicBarrier的計數器能夠被重置後使用,所以它被稱爲是循環的。
  • 說說 Semaphore 原理

    在一個停車場中,車位是公共資源,每輛車就比如一個線程,看門人起的就是信號量的做用。

    信號量是一個非負整數,表示了當前公共資源的可用數目,當一個線程要使用公共資源時,首先要查看信號量,若是信號量的值大於1,則將其減1,而後去佔有公共資源。若是信號量的值爲0,則線程會將本身阻塞,直到有其餘線程釋放公共資源。

    在信號量上咱們定義兩種操做:acquire(獲取)和release(釋放)。當一個線程調用acquire操做時,它要麼經過成功獲取信號量(信號量-1),要麼一直等下去,直到有線程釋放信號量,或超時。release(釋放)實際上會將信號量的值加1,而後喚醒等待線程。

    信號量主要用於兩個目的,一個是用於多個共享資源的互斥使用,另外一個用於併發線程數的控制。

  • 說說 Exchanger 原理

Exchanger ————交換器,是JDK1.5時引入的一個同步器,從字面上就能夠看出,這個類的主要做用是交換數據。

Exchanger有點相似CyclicBarrier,CyclicBarrier是一個柵欄,到達柵欄的線程須要等待其餘必定數量的線程到達後,才能經過柵欄。
Exchanger能夠當作是一個雙向柵欄,如上圖:Thread1線程到達柵欄後,會首先觀察有沒有其它線程已到達柵欄,若是沒有就等待,
若是已經有其餘線程(Thread2)已經到達了,就會以成對的方式交換各自攜帶的信息,所以Exchange很是適合於兩個線程之間的數據交換。

Exchanger<String> exchanger=new Exchanger<String>();
exchanger.exchange(tool) tool爲交換的數據
複製代碼
  • ThreadLocal 原理分析

    ThreadLocal簡介:

    ThreadLocal,這個類提供線程局部變量,這些變量與其餘正常的變量的不一樣之處在於,每個訪問該變量的線程在其內部都有一個獨立的初始化的變量副本;ThreadLocal實例變量一般用private static在類中修飾。

    只要ThreadLocal的變量能被訪問,而且線程存活,那每一個線程都會持有ThreadLocal變量的副本。當一個線程結束時,它所持有的全部ThreadLocal相對的實力副本均可被回收。

    ThreadLocal適用於每一個線程須要本身獨立的實例且該實例須要在多個方法中被使用(相同線程數據共享),也就是變量在線程間隔離(不一樣的線程數據隔離)而在方法或類間共享的場景。

    ThreadLocal原理分析: blog.csdn.net/Mrs_chens/a…

    對象實例與ThreadLocal變量的映射關係是由線程Thread來維護的。

    對象實例與ThreadLocal變量的映射關係是存放在一個Map裏面(這個Map是個抽象的Map並非java.util中的Map),
      這個Map是Thread類的一個字段!而真正存放映射關係的Map就是ThreadLocalMap。
      
      在set方法中首先要獲取當前線程,而後經過getMap獲取當前線程的ThreadLocalMap類型的變量threadLocals,若是存在則直接賦值,若是不存
      在則給該線程ThreadLocalMap變量賦值。賦值的時候這裏的this就是調用變量的對象實例自己。
      
      get方法,一樣也是先獲取當前線程的ThreadLocalMap變量,若是存在則返回值,不存在則建立並返回初始值。setInitialValue()
    複製代碼
  • 講講線程池的實現原理

    • 線程池簡介:

      正常狀況下使用線程的時候就會去建立一個線程,可是在併發狀況下線程的數量不少,每一個線程執行一個很短的任務就結束了,這樣頻繁建立線程就會大大下降系統的效率,由於頻繁建立線程和銷貨線程須要時間。

    線程池使得線程能夠複用,執行完一個任務,並不被銷燬,而是能夠繼續執行其餘的任務。

    線程池的好處,就是能夠方便的管理線程,也能夠減小內存的消耗。

    • 線程池狀態:

      runState: 表示當前線程池的狀態,在ThreadPoolExecutor中爲一個volatile變量,用來保證線程之間的可見性。

      RUNNING: 當建立完線程後的初始值。

      SHUTDOWN: 調用shutdow()方法後,此時線程池不能接受新的任務,它會等待全部任務執行完畢。

      STOP: 調用shutdownnow()方法後,此時線程池不能接受新的任務,而且會去嘗試終止正在執行的任務。

      TERMINATED: 當線程池已處於SHUTDOWN或STOP狀態,而且全部工做線程已經銷燬,任務緩存隊列已經清空或執行結束後,線程池被設置爲TERMINATED狀態。

    • 線程池執行流程:

    任務進來時,首先要執行判斷,判斷核心線程是否處於空閒狀態,
      若是不是,核心線程就會先執行任務,若是核心線程已滿,則判斷任務隊列是否有地方存聽任務,
      若是有,就將任務保存在隊列中,等待執行,若是滿了,在判斷最大可容納的線程數,
      若是沒有超出這個數量就開創非核心線程執行任務,若是超出了,就調用handler實現拒絕策略。
      
      handler的拒絕策略:
      第一種(AbortPolicy):不執行新的任務,直接拋出異常,提示線程池已滿
      第二種(DisCardPolicy):不執行新的任務,也不拋出異常
      第三種(DisCardOldSetPolicy):將消息隊列中的第一個任務替換爲當前新進來的任務執行
      第四種(CallerRunsPolicy):直接調用execute來執行當前任務
    複製代碼
  • 線程池的幾種方式

    • CachedThreadPool: 可緩存的線程池,該線程池中沒有核心線程,非核心線程的數量爲Intger.Max_value,就是無限大,當有須要時建立線程來執行任務,沒有須要時回收線程,適用於耗時少,任務量大的狀況。
    • SecudleThreadPool: 週期性執行任務的線程池,按照某種特定的計劃執行線程中的任務,有核心線程,但也有非核心線程,非核心線程的大小也爲無限大。適用於執行週期性的任務。
    • SingleThreadPool: 只有一條線程來執行任務,適用於有順序的任務的應用場景。
    • FixedThreadPool: 定長的線程池,有核心線程,核心線程的即爲最大的線程數量,沒有非核心線程。
  • 線程的生命週期

第一步是用new Thread()的方法新建一個線程,在線程建立完成以後,線程就進入了就緒狀態(Runnable),
此時建立出來的線程進入搶佔CPU資源的狀態,當線程搶到了CPU的執行權以後,線程就進入了運行狀態(Running),
當線程的任務執行完成以後或者是很是態的調用stop()方法以後,線程就進入了死亡狀態。

如下幾種狀況容易形成線程阻塞:
1. 當線程主動調用了sleep()方法時,線程會進入阻塞狀態;
2. 當線程主動調用了阻塞時的IO方法時,這個方法有一個返回參數,當參數返回以前,線程也會進入阻塞狀態;
3. 當線程進入正在等待某個通知時,會進入阻塞狀態;

如何跳出阻塞過程:
1. 當sleep()方法的睡眠時長過去後,線程就自動跳出了阻塞狀態
2. 第二種則是在返回一個參數以後,在獲取到了等待的通知時,就自動跳出了線程的阻塞過程。
複製代碼

【基礎篇】- 鎖

  • 什麼是線程安全

blog.csdn.net/csdnnews/ar…

當多個線程訪問某個方法時,無論你經過怎麼的調用方式,或者說這些線程如何交替的執行,咱們在主程序中不須要去作任何的同步,這個類的結果行爲都是咱們設想的正確行爲,咱們就說這個類是線程安全的。

無狀態的對象是線程安全的(代碼中不包含任何的做用域,也沒有引用其餘類中的域)。
複製代碼
  • 如何確保線程安全?

    • synchronized:用來控制線程同步,保證在多線程環境下,不被多個線程同時執行,確保數據的完整性,通常是加在方法上。當synchronized鎖住一個對象後,別的線程要想獲取鎖對象,那麼就必須等這個線程執行完釋放鎖對象以後纔可使用。
    public class ThreadDemo {
           int count = 0; // 記錄方法的命中次數
           public synchronized void threadMethod(int j) {
               count++ ;
               int i = 1;
               j = j + i;
           }
        }
    複製代碼
    • Lock:Lock是在java1.6被引入進來的,Lock的引入讓鎖有了可操做性,在須要的時候去手動的獲取鎖和釋放鎖

      • Lock()在獲取鎖的時候,若是拿不到鎖,就一直處於等待狀態,直到拿到鎖
      • tryLock()是有一個Boolean的返回值的,若是沒有拿到鎖,直接返回false,中止等待,它不會像Lock()那樣去一直等待獲取鎖,tryLock()是能夠設置等待的相應時間的。
    private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子類
    
    private void method(Thread thread){
       lock.lock(); // 獲取鎖對象
       try {
           System.out.println("線程名:"+thread.getName() + "得到了鎖");
           // Thread.sleep(2000);
       }catch(Exception e){
           e.printStackTrace();
       } finally {
           System.out.println("線程名:"+thread.getName() + "釋放了鎖");
           lock.unlock(); // 釋放鎖對象
       }
    }
    複製代碼
  • synchronized 與 lock 的區別

    類別 synchronized lock
    存在層次 java內置關鍵字,在jvm層面 Lock是個java類
    鎖狀態 沒法判斷是否獲取鎖的狀態 能夠判斷是否獲取到鎖
    鎖的釋放 會自動釋放鎖
    (a線程執行完同步代碼會釋放鎖)
    (b線程執行過程當中發生異常會釋放鎖)
    須要在finally中手動釋放鎖
    (unlock()方法釋放鎖)
    不然會形成線程死鎖
    鎖的獲取 使用關鍵字的兩個線程1和線程2
    若是當前線程1得到鎖,線程2等待
    若是線程1阻塞,線程2則會一直等待下去
    若是嘗試獲取不到鎖
    線程能夠不用一直等待就結束了
    鎖類型 可重入,不可中斷,非公平 可重入,可判斷,可公平
    性能 適合代碼少許的同步問題 適合大量同步代碼的同步問題
    ------ ------------ ------------
  • 鎖的類型

    • 可重入鎖: 在執行對象中全部同步方法不用再次得到鎖
    • 可中斷鎖: 在等待獲取鎖過程當中可中斷
    • 公平鎖: 按等待獲取鎖的線程的等待時間進行獲取,等待時間長的具備優先獲取鎖權利
    • 讀寫鎖: 對資源讀取和寫入的時候拆分爲2部分處理,讀的時間能夠多線程一塊兒讀,寫的時候必須同步的寫
  • volatile 實現原理

    volatile一般被比喻成「輕量級的synchronize」,也是併發編程中比較重要的一個關鍵字。和synchronized不一樣,volatile是一個變量修飾符,只能用來修飾變量,沒法修飾方法及代碼塊等。

    使用volatile只須要在聲明一個可能被多線程同時訪問的變量時,使用volatile修飾符就能夠了。

    實現原理:

    爲了提交處理器的執行速度,在處理器和內存之間增長了多級緩存來提高。可是因爲引入了多級緩存,就存在緩存數據不一致問題。
      
      可是對於volatile變量,當對volatile變量進行讀寫操做的時候,jvm會向處理器發送一條lock前綴的指令,將這個緩存中的變量回寫到系統主存中。
      
      可是就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算機操做就會有問題,因此在多處理器下,爲了保證每一個處理器的緩存是一致的,
      就會實現緩存一致性協議。
      
      緩存一致性協議:每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,
      就會將當前處理器的緩存行設置成無效狀態,當處理器要對這個數據進行修改操做的時候,會強制從新從系統內存裏把數據讀處處理器緩存裏。
      
      因此,若是一個變量被volatile所修飾的話,在每次數據變化以後,其值都會被強制刷入主存。而其餘處理器的緩存因爲遵照了緩存一致性協議,
      也會把這個變量的值從主存加載到本身的緩存中。這就保證了一個volatile在併發編程中,其值在多個緩存中是可見的。
      
      可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。
    複製代碼
  • synchronized 實現原理

    參考連接: blog.csdn.net/javazejian/…

    synchronized是基於Java對象頭的同步鎖

    synchronized實現同步的基礎:java中的每個對象均可以做爲鎖

    • 對於普通方法,鎖是當前實例對象
    • 對於靜態同步方法,鎖是當前類的Class對象
    • 對於同步方法塊,鎖是synchonized括號裏配置的對象

    【概念】monitor: 每個對象都有一個監視器鎖(monitor),當線程執行時對對象進行加鎖,實際上就是將對象的monitor的狀態設置爲鎖定狀態,monitorenter指令執行的就是這個動做;線程對對象釋放鎖就是執行monitorexit指令,將對象的monitor的狀態置爲無鎖狀態(假設咱們先不考慮鎖的優化)。

    實現原理:

    1. Java虛擬機中的同步(Synchronization)基於進入和退出管理(Monitor)對象實現的。在Java語言中,同步用的最多的地方多是被synchronized修飾的同步方法。同步方法不是由monitorenter和monitorexit指令來實現同步的,而是由方法調用指令讀取運行時常量池中方法的ACC_SYNCHRONIZED標誌來隱式實現的。

    • 實例變量:存放類的屬性數據信息,包括父類的屬性信息,若是是數組的實例部分還包括數組的長度,這部份內存按4字節對齊。

    • 填充數據:因爲虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊,瞭解便可。

    • 對象頭,它是實現synchronized鎖對象的基礎。synchronized使用的鎖對象是存儲在java對象頭裏的,jvm中採用2個字節來存儲對象頭(若是對象是數組則會分配3個字,多出來的1個字記錄的是數組長度),其主要結構由如下組成:

      虛擬機位數 頭對象結構 說明
      32/64bit Mark Word 存儲對象的hashCode,鎖信息或
      或分代年齡或GC標識等信息
      32/64bit Class Metadata Address 類型指針指向對象的類元數據,JVM
      經過這個指針肯定該對象是哪一個類的實例

      其中Mark Word在默認狀況下存儲着對象的HashCode、分代年齡,鎖標記等如下是32位JVM的Mard Word默認存儲結構

      鎖狀態 25bit 4bit 1bit
      是不是偏向鎖
      2bit
      鎖標誌位
      無鎖狀態 對象HashCode 對象分代年齡 0 01

      因爲對象頭的信息是與對象自身定義的數據沒有關係的額外存儲成本,所以考慮到JVM的空間效率,Mark Word 被設計成一個非固定的數據結構,以便存儲更多有效的數據,它會根據對象自己的狀態複用本身的存儲空間。

    1. 輕量級鎖和偏向鎖是Java6對synchronized鎖進行優化後新增長的,重量級鎖也就是一般說synchronized的對象鎖,鎖標識位爲10,其中指針指向的是monitor對象(也稱爲管理或監視器鎖)的起始地址。每一個對象都存在着一個monitor與之關聯,對象與其monitor之間的關係存在多種實現方式。如monitor能夠與對象一塊兒建立銷燬或當線程試圖獲取對象鎖時自動生成,但當一個monitor被某個線程持有後,它便處於鎖定狀態。在java虛擬機中,monitor是由ObjectMonitor實現的(C++實現)。

    2. ObjectMonitor中有兩個隊列,_WaitSet和_EntryList,用來保存ObjectWaiter對象列表(每一個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入_EntryList集合,當線程獲取到對象的monitor後進入_Owner區域並把monitor中的owner變量設置爲當前線程同時monitor中的計數器count加1,若線程調用wait()方法,將釋放當前持有的monitor,owner變量恢復爲null,count自減1,同時該線程進入WaitSet集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其餘線程進入獲取monitor(鎖)。

    3. 由此看來,monitor對象存在於每一個java對象的對象頭中(存儲的指針的指向),synchronized鎖即是經過這種方式獲取鎖的,也是爲何java中任意對象能夠做爲鎖的緣由,同時也是notify/motifyAll/Wait等方法存在於頂級對象Object中的緣由。

  • volatile和synchronized區別

    • volatile:本質是在告訴jvm當前變量在寄存器(工做內存)中的值是不肯定的,須要從主存中讀取;

      synchronized:則是鎖定當前變量,只有當線程能夠訪問該變量,其餘線程被阻塞住。

    • volatile:僅能使用在變量級別;

      synchronized:則可使用在變量,方法和類級別的。

    • volatile:僅能實現變量的修改可見性,不能保證原子性;

      synchronized:則能夠保證變量的修改可見性和原子性。

    • volatile:不會形成線程的阻塞;

      synchronized:可能會形成線程的阻塞。

    • volatile:標記的變量不會被編譯器優化;

      synchronized:標記的變量能夠被編譯器優化。

  • CAS 樂觀鎖

    blog.csdn.net/qq_35571554…

    • 悲觀鎖:

      獨佔鎖是一種悲觀鎖,而synchronized就是一種獨佔鎖,synchronized會致使其它全部未持有的鎖的線程阻塞,而等待持有鎖的線程釋放鎖。 synchronized屬於悲觀鎖,悲觀地認爲程序中的併發狀況嚴重,因此嚴防死守。

    • 樂觀鎖:

      每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止,而樂觀鎖用到的機制就是CAS。

    • CAS(Compare And Swap)(比較並替換):

      CAS機制當中使用了3個基本操做數:內存地址V,舊的預期值A,要修改的新值B

      更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時,纔會將內存地址V對應的值修改成B。

      CAS底層利用了unsafe提供了原子性操做方法。

      例如:

      1. 在內存地址V當中,存儲着值爲10的變量。
      2. 此時線程1想要把變量的值增長1。對線程1來講,舊的預期值A=10,要修改的新值B=11。
      3. 在線程1要提交更新以前,另外一個線程2搶先一步,把內存地址V中的變量值率先更新成了11。
      4. 線程1開始提交更新,首先進行A和地址V的實際值比較(Compare),發現A不等於V的實際值,提交失敗。
      5. 線程1從新獲取內存地址V的當前值,並從新計算想要修改的新值。此時對線程1來講,A=11,B=12。這個從新嘗試的過程被稱爲自旋。
      6. 這一次比較幸運,沒有其餘線程改變地址V的值。線程1進行Compare,發現A和地址V的實際值是相等的。
      7. 線程1進行SWAP,把地址V的值替換爲B,也就是12。
    • CAS的缺點:

      1. CPU開銷較大: 在併發量比較高的狀況下,若是許多線程反覆嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很大的壓力。

      2. 不能保證代碼塊的原子性: CAS機制所保證的只是一個變量的原子性操做,而不能保證整個代碼塊的原子性。好比須要保證3個變量共同進行原子性的更新,就不得不使用Synchronized了。

      由於它自己就只是一個鎖住總線的原子交換操做啊。兩個CAS操做之間並不能保證沒有重入現象。

  • ABA 問題

    • 能夠發現,CAS實現的過程是先取出內存中某時刻的數據,在下一時刻比較並替換,那麼在這個時間差會致使數據的變化,此時就會致使出現「ABA」問題。

    • 什麼是」ABA」問題? 好比說一個線程one從內存位置V中取出A,這時候另外一個線程two也從內存中取出A,而且two進行了一些操做變成了B,而後two又將V位置的數據變成A,這時候線程one進行CAS操做發現內存中仍然是A,而後one操做成功。儘管線程one的CAS操做成功,可是不表明這個過程就是沒有問題的。

      例如:

      1. 從取款機取50塊錢,餘額爲100
      2. 當線程1執行成功後,當前餘額爲50,因爲內存地址的值改變,致使線程2阻塞
      3. 這時正好有轉帳50元信息,線程3執行成功
      4. 當線程2在自旋的過程當中檢測到內存地址的值與舊的預期值是一致的,因此就會再次進行取款操做,正常狀況下線程2應該是執行失敗的,結果因爲ABA的問題提交成功了。
    • 解決ABA問題

      當一個值從A更新到B,又更新爲A,普通的CAS機制會誤判經過檢測。

      利用版本號比較就能夠有效解決ABA問題。

  • 樂觀鎖的業務場景及實現方式

    • 樂觀鎖的應用場景: 在多節點部署或者多線程執行時,同一個時間可能有多個線程更新相同數據,產生衝突,這就是併發問題。這樣的狀況下會出現如下問題:

      • 更新丟失:一個事務更新數據後,被另外一個更新數據的事務覆蓋。
      • 髒讀:一個事務讀取另外一個事務爲提交的數據,即爲髒讀。
      • 其次還有幻讀。

      針對併發引入控制機制,即加鎖。

      加鎖的目的是在同一時間只有一個事務在更新數據,經過鎖獨佔數據的修改權。

    • 樂觀鎖的實現方式:

      • version方式:通常是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,不然重試更新操做,知道操做成功。
      update table set x=x+1, version=version+1 where id=#{id} and version=#{version}; 
      複製代碼
      • CAS操做方式:即compare and swap或者compare and set,涉及到三個操做數,數據所在的內存值,預期值,新值。當須要更新時,判斷當前內存值與以前取到的值是否相等,若相等,則用新值更新,若失敗則重試,通常狀況下是一個自旋操做嗎,即不斷的重試。

更詳細的面試總結連接請戳:👇👇
juejin.im/post/5db8d9…

【推薦篇】- 書籍內容整理筆記 連接地址
【推薦】【Java編程思想】【筆記】 juejin.im/post/5dbb7a…
【推薦】【Java核心技術 卷Ⅰ】【筆記】 juejin.im/post/5dbb7b…

如有錯誤或者理解不當的地方,歡迎留言指正,但願咱們能夠一塊兒進步,一塊兒加油!😜😜

相關文章
相關標籤/搜索