1. 線程鎖的原理是什麼
線程鎖的原理:當對象獲取鎖時,它首先使自己的高速緩存無效,這樣就可以保證直接從主內存中裝入變數。
同樣,在對象釋放鎖之前,它會刷新其高速緩存,強制使已做的任何更改都出現在主內存中。 這樣,會保證在同一個鎖上同步的兩個線程看到在 synchronized 塊內修改的變數的相同值。
一般來說,線程以某種不必讓其他線程立即可以看到的方式(不管這些線程在寄存器中、在處理器特定的緩存中,還是通過指令重排或者其他編譯器優化),不受緩存變數值的約束。
線程鎖在run()函數中使用QMutex實現同步,當多個線程訪問共享變數時,使用lock/trylock和unlock將共享變數包裹,以保證同步訪問共享變數。
如果不加鎖將會在2秒後同時修改num變數,將會導致線程不按照我們的想法執行,當前線程鎖定後,其他線程如果遇到共享變數將會等待解鎖;
使用QMutex上鎖解鎖時,當代碼提前退出有可能並未執行unlock(),若其他線程採用lock上鎖會一直被阻塞,導致內存溢出。
2. java中volatile修飾的變數有什麼特徵
volatile是一個類型修飾符,它是被設計用來修飾被不同線程訪問和修改的變數,可以被非同步的線程所修改。
final必須對它賦予初值並且不能修改它。
對比就知道兩個修飾符是沖突的,放一起是要干什麼呢?
3. Java內存模型詳解
理解Java內存模型是深入學習Java並發不可或缺的部分。Java內存模型即JavaMemoryModel,簡稱為JMM,定義了多線程之間共享變數的可見性以及如何在需要的時候對共享變數進行同步。
JMM規定Java線程間的通信採用共享內存的方式。在Java中,所有成員變數、靜態變數和數組元素都存儲在堆內存中,堆內存在線程之間共享,所以它們通常也稱為共享變數。
JMM定義了線程和主內存之間的抽象關系:線程之間的共享變數存儲在主內存(MainMemory)中,每個線程都有一個私有的本地內存(LocalMemory,或者也可以稱為工作內存WorkMemory),本地內存中存儲了該線程以讀/寫共享變數的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存、寫緩沖區、寄存器以及其他的硬體和編譯器優化。
JMM抽象JMM的抽象示意圖如下所示:
多個線程同時對同一個共享變數進行讀寫的時候會產生線程安全問題。那為什麼CPU不直接操作內存,而要在CPU和內存間加上各種緩存和寄存器等緩沖區呢?因為CPU的運算速度要比內存的讀寫速度快得多,如果CPU直接操作內存的話勢必會花費很長時間等待數據到來,所以緩存的出現主要是為了解決CPU運算速度與內存讀寫速度不匹配的矛盾。
內存間交互協議JMM規定了主內存和工作內存間具體的交互協議,即一個變數如何從主內存拷貝到工作內存、如何從工作內存同步到主內存之間的實現細節,這主要包含了下面8個步驟:
lock(鎖定):作用於主內存的變數,把一個變數標識為一條線程獨占狀態。
unlock(解鎖):作用於主內存變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他線程鎖定。
read(讀取):作用於主內存變數,把一個變數值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用
load(載入):作用於工作內存的變數,它把read操作從主內存中得到的變數值放入工作內存的變數副本中。
use(使用):作用於工作內存的變數,把工作內存中的一個變數值傳遞給執行引擎,每當虛擬機遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。
assign(賦值):作用於工作內存的變數,它把一個從執行引擎接收到的值賦值給工作內存的變數,每當虛擬機遇到一個給變數賦值的位元組碼指令時執行這個操作。
store(存儲):作用於工作內存的變數,把工作內存中的一個變數的值傳送到主內存中,以便隨後的write的操作。
write(寫入):作用於主內存的變數,它把store操作從工作內存中一個變數的值傳送到主內存的變數中。lock,unlock需要代碼中用鎖實現。
這8個步驟必須符合下述規則:
不允許read和load,store和write操作之一單獨出現。
不允許一個線程丟棄它最近的assign操作。即變數在工作內存中改變了賬號必須把變化同步回主內存。
一個新的變數只允許在主內存中誕生,不允許工作內存直接使用未初始化的變數。
一個變數同一時刻只允許一條線程進行lock操作,但同一線程可以lock多次,lock多次之後必須執行同樣次數的unlock操作。
如果對一個變數進行lock操作,那麼將會清空工作內存中此變數的值。
不允許對未lock的變數進行unlock操作,也不允許unlock一個被其它線程lock的變數。
如果一個變數執行unlock操作,必須先把此變數同步回主內存中。
指令重排在執行程序時,為了提高性能,編譯器和處理器常常會對指令做重排序。從Java源代碼到最終實際執行的指令序列,會分別經歷下面3種重排序:
編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-LevelParallelism,ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
內存系統的重排序。由於處理器使用緩存和讀/寫緩沖區,這使得載入和存儲操作看上去可能是在亂序執行。
如果兩個操作訪問同一個變數,其中一個為寫操作,此時這兩個操作之間存在數據依賴性。編譯器和處理器不會改變存在數據依賴性關系的兩個操作的執行順序,即不會重排序。不管怎麼重排序,單線程下的執行結果不能被改變,編譯器、runtime和處理器都必須遵守as-if-serial語義。
內存屏障通過插入內存屏障(MemoryBarrier)可以阻止特定類型的指令重排。JMM將內存屏障劃分為四種:
屏障類型示例描述LoadLoadBarriersLoad1-LoadLoad-Load2Load1數據裝載過程要先於Load2及所有後續的數據裝載過程StoreStoreBarriersStore1-StoreStore-Store2Store1刷新數據到內存的過程要先於Strore2及後續所有刷新數據到內存的過程LoadStoreBarriersLoad1-LoadStore-Store2Load1數據裝載要先於Strore2及後續所有刷新數據到內存的過程StoreLoadBarriersStore1-StoreLoad-Load2Store1刷新數據到內存的過程要先於Load2及所有後續的數據裝載過程Java中volatile關鍵字的實現就是通過內存屏障來完成的。
happens-before從jdk5開始,java使用新的JSR-133內存模型,基於happens-before的概念來闡述操作之間的內存可見性。
在JMM中,如果一個操作的執行結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關系,這個的兩個操作既可以在同一個線程,也可以在不同的兩個線程中。
與程序員密切相關的happens-before規則如下:
程序順序規則:一個線程中的每個操作,happens-before於該線程中任意的後續操作。
監視器鎖規則:對一個鎖的解鎖操作,happens-before於隨後對這個鎖的加鎖操作。
volatile域規則:對一個volatile域的寫操作,happens-before於任意線程後續對這個volatile域的讀。
傳遞性規則:如果Ahappens-beforeB,且Bhappens-beforeC,那麼Ahappens-beforeC。
注意:兩個操作之間具有happens-before關系,並不意味前一個操作必須要在後一個操作之前執行!僅僅要求前一個操作的執行結果,對於後一個操作是可見的,且前一個操作按順序排在後一個操作之前。