再快不能快基礎,再爛不能爛語言!
【基礎篇】- 執行緒
執行緒:一個程式同時執行多個任務,通常,每一個任務稱為一個執行緒。
序列: 對於單條執行緒執行多個任務,例如下載多個任務,需要下載完一個再下載另一個。
並行:下載多個檔案,開啟多條執行緒,多個檔案同時下載。
-
建立執行緒的方式及實現
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():
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實現原理:
CountDownLatch是通過“共享鎖”實現的。在建立CountDownLatch中時,會傳遞一個int型別引數count,該引數是“鎖計數器”的初始狀態,
表示該“共享鎖”最多能被count個執行緒同時獲取。當某執行緒呼叫該CountDownLatch物件的await()方法時,該執行緒會等待“共享鎖”可用時,
才能獲取“共享鎖”進而繼續執行,而“共享鎖”可用的條件,就是“鎖計數器”的值為0!而“鎖計數器”的初始值為count,每當一個執行緒呼叫
該CountDownLatch物件的CountDown()方法時,才將“鎖計數器”-1;通過這種方式,必須有count個執行緒呼叫countDown()之後,
“鎖計數器”才為0,而前面提到的等待執行緒才能繼續執行!
複製程式碼
-
說說 CyclicBarrier 原理
CyclicBarrier簡介:
柵欄類似於閉鎖,它能阻塞一執行緒直到某個事件傳送。柵欄與閉鎖的關鍵區別在於,所有執行緒必須同時到達柵欄位置,才能繼續執行。閉鎖用於等待時間,而柵欄用於等待其他執行緒。
所有執行緒相互等待,直到所有的執行緒到達某一點時才開啟柵欄,然後執行緒可以繼續執行
CyclicBarrier實現原理:
CyclicBarrier的原始碼實現和CountDownLatch大相庭徑,CyclicBarrier基於Condition來實現的。CyclicBarrier類的內部有一個計數器,
每個執行緒在到達屏障點的時候都會呼叫await方法將自己阻塞,此事計數器會減1,當計數器為0的時候所有因呼叫await方法而被阻塞的執行緒將被喚醒。
複製程式碼
-
說說 CountDownLatch 與 CyclicBarrier 區別
- 這兩個類都可以實現一組執行緒在到達某個條件之前進行等待,它們內部都有一個計數器,當計數器的值不斷的減為0的時候所有阻塞的執行緒將會被喚醒。
- CountDownLatch的計數器是有使用者來控制的,呼叫await方法只是將自己阻塞而不會減少計數器的值。
CyclicBarrier的計數器是由自己控制,呼叫await方法不僅會將自己阻塞還會將減少計數器的值。 - CountDownLatch只能攔截一輪 CyclicBarrier可以實現迴圈攔截(CyclicBarrier可以實現CountDownLatch的功能,反之則不能)
- CountDownLatch的作用是允許1或N個執行緒等待其他執行緒完成執行;
CyclicBarrier則是允許N個執行緒相互等待。 - CountDownLactch的計數器無法被重置;
CyclicBarrier的計數器可以被重置後使用,因此它被稱為是迴圈的。
-
說說 Semaphore 原理
在一個停車場中,車位是公共資源,每輛車就好比一個執行緒,看門人起的就是訊號量的作用。
訊號量是一個非負整數,表示了當前公共資源的可用數目,當一個執行緒要使用公共資源時,首先要檢視訊號量,如果訊號量的值大於1,則將其減1,然後去佔有公共資源。如果訊號量的值為0,則執行緒會將自己阻塞,直到有其他執行緒釋放公共資源。
在訊號量上我們定義兩種操作:acquire(獲取)和release(釋放)。當一個執行緒呼叫acquire操作時,它要麼通過成功獲取訊號量(訊號量-1),要麼一直等下去,直到有執行緒釋放訊號量,或超時。release(釋放)實際上會將訊號量的值加1,然後喚醒等待執行緒。
訊號量主要用於兩個目的,一個是用於多個共享資源的互斥使用,另一個用於併發執行緒數的控制。
-
說說 Exchanger 原理
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. 第二種則是在返回一個引數之後,在獲取到了等待的通知時,就自動跳出了執行緒的阻塞過程。
複製程式碼
【基礎篇】- 鎖
-
什麼是執行緒安全
當多個執行緒訪問某個方法時,不管你通過怎麼的呼叫方式,或者說這些執行緒如何交替的執行,我們在主程式中不需要去做任何的同步,這個類的結果行為都是我們設想的正確行為,我們就說這個類是執行緒安全的。
無狀態的物件是執行緒安全的(程式碼中不包含任何的作用域,也沒有引用其他類中的域)。
複製程式碼
-
如何確保執行緒安全?
- 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的狀態置為無鎖狀態(假設我們先不考慮鎖的優化)。
實現原理:
- 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 被設計成一個非固定的資料結構,以便儲存更多有效的資料,它會根據物件本身的狀態複用自己的儲存空間。
-
輕量級鎖和偏向鎖是Java6對synchronized鎖進行優化後新增加的,重量級鎖也就是通常說synchronized的物件鎖,鎖標識位為10,其中指標指向的是monitor物件(也稱為管理或監視器鎖)的起始地址。每個物件都存在著一個monitor與之關聯,物件與其monitor之間的關係存在多種實現方式。如monitor可以與物件一起建立銷燬或當執行緒試圖獲取物件鎖時自動生成,但當一個monitor被某個執行緒持有後,它便處於鎖定狀態。在java虛擬機器中,monitor是由ObjectMonitor實現的(C++實現)。
-
ObjectMonitor中有兩個佇列,_WaitSet和_EntryList,用來儲存ObjectWaiter物件列表(每個等待鎖的執行緒都會被封裝成ObjectWaiter物件),_owner指向持有ObjectMonitor物件的執行緒,當多個執行緒同時訪問一段同步程式碼時,首先會進入_EntryList集合,當執行緒獲取到物件的monitor後進入_Owner區域並把monitor中的owner變數設定為當前執行緒同時monitor中的計數器count加1,若執行緒呼叫wait()方法,將釋放當前持有的monitor,owner變數恢復為null,count自減1,同時該執行緒進入WaitSet集合中等待被喚醒。若當前執行緒執行完畢也將釋放monitor(鎖)並復位變數的值,以便其他執行緒進入獲取monitor(鎖)。
-
由此看來,monitor物件存在於每個java物件的物件頭中(儲存的指標的指向),synchronized鎖便是通過這種方式獲取鎖的,也是為什麼java中任意物件可以作為鎖的原因,同時也是notify/motifyAll/Wait等方法存在於頂級物件Object中的原因。
-
volatile和synchronized區別
-
volatile:本質是在告訴jvm當前變數在暫存器(工作記憶體)中的值是不確定的,需要從主存中讀取;
synchronized:則是鎖定當前變數,只有當執行緒可以訪問該變數,其他執行緒被阻塞住。
-
volatile:僅能使用在變數級別;
synchronized:則可以使用在變數,方法和類級別的。
-
volatile:僅能實現變數的修改可見性,不能保證原子性;
synchronized:則可以保證變數的修改可見性和原子性。
-
volatile:不會造成執行緒的阻塞;
synchronized:可能會造成執行緒的阻塞。
-
volatile:標記的變數不會被編譯器優化;
synchronized:標記的變數可以被編譯器優化。
-
-
CAS 樂觀鎖
-
悲觀鎖:
獨佔鎖是一種悲觀鎖,而synchronized就是一種獨佔鎖,synchronized會導致其它所有未持有的鎖的執行緒阻塞,而等待持有鎖的執行緒釋放鎖。 synchronized屬於悲觀鎖,悲觀地認為程式中的併發情況嚴重,所以嚴防死守。
-
樂觀鎖:
每次不加鎖而是假設沒有衝突而去完成某項操作,如果因為衝突失敗就重試,直到成功為止,而樂觀鎖用到的機制就是CAS。
-
CAS(Compare And Swap)(比較並替換):
CAS機制當中使用了3個基本運算元:記憶體地址V,舊的預期值A,要修改的新值B
更新一個變數的時候,只有當變數的預期值A和記憶體地址V當中的實際值相同時,才會將記憶體地址V對應的值修改為B。
CAS底層利用了unsafe提供了原子性操作方法。
例如:
- 在記憶體地址V當中,儲存著值為10的變數。
- 此時執行緒1想要把變數的值增加1。對執行緒1來說,舊的預期值A=10,要修改的新值B=11。
- 線上程1要提交更新之前,另一個執行緒2搶先一步,把記憶體地址V中的變數值率先更新成了11。
- 執行緒1開始提交更新,首先進行A和地址V的實際值比較(Compare),發現A不等於V的實際值,提交失敗。
- 執行緒1重新獲取記憶體地址V的當前值,並重新計算想要修改的新值。此時對執行緒1來說,A=11,B=12。這個重新嘗試的過程被稱為自旋。
- 這一次比較幸運,沒有其他執行緒改變地址V的值。執行緒1進行Compare,發現A和地址V的實際值是相等的。
- 執行緒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操作成功,但是不代表這個過程就是沒有問題的。
例如:
- 從取款機取50塊錢,餘額為100
- 當執行緒1執行成功後,當前餘額為50,由於記憶體地址的值改變,導致執行緒2阻塞
- 這時正好有轉賬50元資訊,執行緒3執行成功
- 當執行緒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… |
若有錯誤或者理解不當的地方,歡迎留言指正,希望我們可以一起進步,一起加油!??