是指一個線程對共享變量進行修改,另外一個先當即獲得修改後的最新值。java
代碼演示:面試
public class Test01Visibility { public static boolean flag = true; public static void main(String[] args) throws InterruptedException { new Thread(() -> { while (flag) { } }).start(); Thread.sleep(2000); new Thread(() -> { flag = false; System.out.println("修改了flag"); }).start(); } }
小結:併發編程時,會出現可見性問題,當一個線程對共享變量進行了修改,另外的線程並無當即看到修改編程
後的最新值。數組
在一次或屢次操做中,要麼全部的操做都執行而且不會受其餘因素干擾而中斷,要麼全部的操做都不執行緩存
代碼演示:安全
public class Test02Atomicity { public static int num = 0; public static void main(String[] args) throws InterruptedException { // 建立任務 Runnable increment = () -> { for (int i = 0; i < 1000; i++) { num++; } }; ArrayList<Thread> threads = new ArrayList<>(); //建立線程 for (int i = 0; i < 5; i++) { Thread t = new Thread(increment); t.start(); threads.add(t); } for (Thread thread : threads) { thread.join(); } System.out.println(num); } }
經過 javap -p -v Test02Atomicity
對class 文件進行反彙編:發現++ 操做是由4條字節碼指令組成,並非原子操做多線程
小結:併發編程時,會出現原子性問題,當一個線程對共享變量操做到一半時,另外的線程也有可能來操做共享變量,干擾了前一個線程的操做併發
是指程序中代碼的執行順序,Java在編譯時和運行時會對代碼進行優化,會致使程序最終的執行順序不必定就是咱們編寫代碼時的順序。ide
代碼演示:高併發
@JCStressTest @Outcome(id={"1","4"},expect=Expect.ACCEPTABLE,desc="ok") @Outcome(id="0",expect=Expect.ACCEPTABLE_INTERESTING,desc="danger") @State public class Test03Orderliness { int num=0; boolean ready=false; //線程一執行的代碼 @Actor public void actor1(I_Resultr){ if(ready){ r.r1=num+num; }else{ r.r1=1; } } //線程2執行的代碼 @Actor public void actor2(I_Resultr){ num=2; ready=true; } }
運行的結果有:0、一、4
小結:程序代碼在執行過程當中的前後順序,因爲Java在編譯期以及運行期的優化,致使了代碼的執行順序未必
就是開發者編寫代碼時的順序。
根據馮諾依曼體系結構,計算機由五大組成部分,輸入設備,輸出設備,存儲器,控制器,運算器。
CPU:
中央處理器,是計算機的控制和運算的核心,咱們的程序最終都會變成指令讓CPU去執行,處理程序中的數據。
內存:
咱們的程序都是在內存中運行的,內存會保存程序運行時的數據,供CPU處理。
緩存:
CPU的運算速度和內存的訪問速度相差比較大。這就致使CPU每次操做內存都要耗費不少等待時間。因而就有了在
CPU和主內存之間增長緩存的設計。CPU Cache分紅了三個級別: L1, L2, L3。級別越小越接近CPU,速度也更快,同時也表明着容量越小。
Java內存模型是一套規範,描述了Java程序中各類變量(線程共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取變量這樣的底層細節,具體以下。
主內存
主內存是全部線程都共享的,都能訪問的。全部的共享變量都存儲於主內存。
工做內存
每個線程有本身的工做內存,工做內存只存儲該線程對共享變量的副本。線程對變量的全部的操做(讀,取)都必須在工做內存中完成,而不能直接讀寫主內存中的變量,不一樣線程之間也不能直接訪問對方工做內存中的變量。
小結
Java內存模型是一套規範,描述了Java程序中各類變量(線程共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取變量這樣的底層細節,Java內存模型是對共享數據的可見性、有序性、和原子性的規則和保障。
注意:1. 若是對一個變量執行lock操做,將會清空工做內存中此變量的值
while(flag){ //增長對象共享數據的打印,println是同步方法 System.out.println("run="+run); }
小結:
synchronized保證可見性的原理,執行synchronized時,lock原子操做會刷新工做內存中共享變量的值。
for(int i = 0; i < 1000; i++){ synchronized(Test01Atomicity.class){ number++; } }
小結:
synchronized保證原子性的原理,synchronized保證只有一個線程拿到鎖,可以進入同步代碼塊。
synchronized(Test01Atomicity.class){ num=2; ready=true; }
小結
synchronized保證有序性的原理,咱們加synchronized後,依然會發生重排序,只不過,咱們有同步代碼塊,能夠保證只有一個線程執行同步代碼中的代碼,保證有序性。
public class Demo01 { public static void main(String[] args) { new MyThread().start(); new MyThread().start(); } } class MyThread extends Thread { @Override public void run() { synchronized (MyThread.class) { System.out.println(Thread.currentThread().getName() + "獲取了鎖1"); synchronized (MyThread.class) { System.out.println(Thread.currentThread().getName() + "獲取了鎖2"); } } } }
可重入原理:
synchronized的鎖對象中有一個計數器(recursions變量)會記錄線程得到幾回鎖。
可重入的好處:
能夠避免死鎖
可讓咱們更好的來封裝代碼
小結:
synchronized是可重入鎖,內部鎖對象中會有一個計數器記錄線程獲取幾回鎖啦,獲取一次鎖加+1,在執行完同步代碼塊時,計數器的數量會-1,直到計數器的數量爲0,就釋放這個鎖。
什麼是不可中斷?
一個線程得到鎖後,另外一個線程想要得到鎖,必須處於阻塞或等待狀態,若是第一個線程不釋放鎖,第二個線程會一直阻塞或等待,不可被中斷。
public class Uninterruptible { private static Object obj = new Object(); public static void main(String[] args) throws InterruptedException { Runnable run = () -> { synchronized (obj) { String name = Thread.currentThread().getName(); System.out.println(name + "執行同步代碼塊"); try { Thread.sleep(888888); } catch (InterruptedException e) { e.printStackTrace(); } } }; Thread t1 = new Thread(run); t1.start(); Thread.sleep(1000); Thread t2 = new Thread(run); t2.start(); Thread.sleep(1000); System.out.println("中止線程2前"); System.out.println(t2.getState()); t2.interrupt(); System.out.println("中止線程2後"); System.out.println(t1.getState()); System.out.println(t2.getState()); } }
synchronized是不可中斷,處於阻塞狀態的線程會一直等待鎖。
public class Interruptible { private static Lock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { test01(); } private static void test01() throws InterruptedException { Runnable run = () -> { boolean flag = false; String name = Thread.currentThread().getName(); try { flag = lock.tryLock(3, TimeUnit.SECONDS); if (flag) { System.out.println(name + "得到鎖,進入鎖執行"); Thread.sleep(888888); } else { System.out.println(name + "沒有得到鎖"); } } catch (InterruptedException e) { e.printStackTrace(); } finally { if (flag) { lock.unlock(); System.out.println(name + "釋放鎖"); } } }; Thread t1 = new Thread(run); t1.start(); Thread.sleep(1000); Thread t2 = new Thread(run); t2.start(); } }
小結:
synchronized屬於不可被中斷
Lock的lock方法是不可中斷的
Lock的tryLock方法是可中斷的
每個對象都會和一個監視器monitor關聯。監視器被佔用時會被鎖住,其餘線程沒法來獲取該monitor。當JVM執行某個線程的某個方法內部的monitorenter時,它會嘗試去獲取當前對象對應的monitor的全部權。其過程以下:
若monior的進入數爲0,線程能夠進入monitor,並將monitor的進入數置爲1。當前線程成爲monitor的owner(全部者)
若線程已擁有monitor的全部權,容許它重入monitor,則進入monitor的進入數加1
若其餘線程已經佔有monitor的全部權,那麼當前嘗試獲取monitor的全部權的線程會被阻塞,直到monitor的進入數變爲0,才能從新嘗試獲取monitor的全部權。
monitorenter小結:
synchronized的鎖對象會關聯一個monitor, 這個monitor不是咱們主動建立的, 是JVM的線程執行到這個同步代碼塊,發現鎖對象
有monitor就會建立monitor, monitor內部有兩個重要的成員變量owner擁有這把鎖的線程,recursions會記錄線程擁有鎖的次數,
當一個線程擁有monitor後其餘線程只能等待。
能執行monitorexit 指令的線程必定是擁有當前對象的monitor的全部權的線程。
執行monitorexit 時會將monitor的進入數減1。當monitor的進入數減爲0時,當前線程退出monitor,再也不擁有monitor的全部權,此時其餘被這個monitor阻塞的線程能夠嘗試去獲取這個monitor的全部權
monitorexit釋放鎖。
monitorexit插入在方法結束處和異常處,JVM保證每一個monitorenter必須有對應的monitorexit。
面試題synchroznied出現異常會釋放鎖嗎?
:會釋放鎖。
同步方法在反彙編後,會增長ACC_SYNCHRONIZED修飾。會隱式調用monitorenter 和monitorexit。在執行同步方法前會調用
monitorenter,在執行完同步方法後會調用monitorexit 。
小結:
經過javap反彙編能夠看到synchronized 使用了monitorentor和monitorexit兩個指令。每一個鎖對象都會關聯一個monitor(監視
器,它纔是真正的鎖對象),它內部有兩個重要的成員變量owner會保存得到鎖的線程,recursions會保存線程得到鎖的次數, 當執行到
monitorexit時, recursions會-1, 當計數器減到0時這個線程就會釋放鎖。
一、synchronized 是關鍵字,lock 是一個接口
二、synchronized 會自動釋放鎖,lock 須要手動釋放鎖。
三、synchronized 是不可中斷的,lock 能夠中斷也能夠不中斷。
四、經過lock 能夠知道線程有沒有拿到鎖,而synchronized 不能。
五、synchronized 能鎖住方法和代碼塊,而lock 只能鎖住代碼塊。
六、lock 可使用讀鎖提升多線程讀效率。
七、synchronized 是非公平鎖,ReentrantLock 能夠控制是不是公平鎖。
cas的概述和做用:
compare and swap,能夠將比較和交換轉爲原子操做,這個原子操做直接由cpu保證,cas能夠保證共享變量賦值時的原子操做,cas依賴3個值:內存中的值v,舊的預估值x,要修改的新值b。根據atomicInteger的地址加上偏移量offset的值能夠獲得內存中的值,將內存中的值和舊的預估值進行比較,若是相同,就將新值保存到內存中。不相同就進行重試。
在JVM中,對象在內存中的佈局分爲三塊區域:對象頭、實例數據和對齊填充。以下圖所示:
HotSpot採用instanceOopDesc和arrayOopDesc來描述對象頭,arrayOopDesc對象用來描述數組類型。
從instanceOopDesc代碼中能夠看到 instanceOopDesc繼承自oopDesc。
_mark表示對象標記、屬於markOop類型,也就是Mark World,它記錄了對象和鎖有關的信息
_metadata表示類元信息,類元信息存儲的是對象指向它的類元數據(Klass)的首地址,其中Klass表示普通指針、compressed_klass表示壓縮類指針。
Mark Word
鎖狀態 | 存儲內容 | 鎖標誌位 |
---|---|---|
無鎖 | 對象的hashcode、對象分代年齡、是不是偏向鎖(0) | 01 |
偏向鎖 | 偏向線程id、偏向時間戳、對象分代年齡、是不是偏向鎖(1) | 01 |
輕量級鎖 | 指向棧中鎖記錄的指針 | 00 |
重量級鎖 | 指向互斥量(重量級鎖)的指針 | 10 |
klass pointer
用於存儲對象的類型指針,該指針指向它的類元數據,JVM經過這個指針肯定對象是哪一個類的實例。經過-XX:+UseCompressedOops開啓指針壓縮,
在64位系統中,Mark Word = 8 bytes,類型指針 = 8bytes,對象頭 = 16 bytes = 128bits;
實例數據
就是類中定義的成員變量。
對齊填充
因爲HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,對象的大小必須是8字節的整數倍。所以,當對象實例數據部分沒有對齊時,就須要經過對齊填來補全。
查看Java對象佈局
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
小結
Java對象由3部分組成,對象頭,實例數據,對齊數據,對象頭分紅兩部分:Mark World + Klass pointer
什麼是偏向鎖?
鎖會偏向於第一個得到它的線程,會在對象頭存儲鎖偏向的線程ID,之後該線程進入和退出同步塊時只須要檢查是否爲偏向鎖、鎖標誌位以及ThreadID便可。
不過一旦出現多個線程競爭時必須撤銷偏向鎖,因此撤銷偏向鎖消耗的性能必須小於以前節省下來的CAS原子操做的性能消耗,否則就得不償失了。
偏向鎖原理
當線程第一次訪問同步塊並獲取鎖時,偏向鎖處理流程以下:
偏向鎖的撤銷
偏向鎖的撤銷動做必須等待全局安全點
暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態
撤銷偏向鎖,恢復到無鎖(標誌位爲01)或輕量級鎖(標誌位爲00)的狀態
偏向鎖是自適應的
小結:
偏向鎖的原理是什麼?
當鎖對象第一次被線程獲取的時候,虛擬機將會把對象頭中的標誌位設爲「01」,即偏向模式。同時使用CAS操做把獲取到這個鎖的線程的ID記錄在對象的MarkWord之中,若是CAS操做成功,持有偏向鎖的線程之後每次進入這個鎖相關的同步塊時,虛擬機均可以再也不進行任何同步操做,偏向鎖的效率高。
偏向鎖的好處是什麼?
偏向鎖是在只有一個線程執行同步塊時進一步提升性能,適用於一個線程反覆得到同一鎖的狀況。偏向鎖能夠提升帶有同步但無競爭的程序性能。
什麼是輕量級鎖?
輕量級鎖是JDK 6之中加入的新型鎖機制,輕量級鎖並非用來代替重量級鎖的。
引入輕量級鎖的目的:在多線程交替執行同步塊的狀況下,儘可能避免重量級鎖引發的性能消耗,可是若是多個線程在同一時刻進入臨界區,會致使輕量級鎖膨脹升級爲重量級鎖,因此輕量級鎖的出現並不是是要代替重量級鎖。
輕量級鎖原理:
當關閉偏向鎖或多個線程競爭偏向鎖致使偏向鎖升級爲輕量級鎖,則會嘗試獲取輕量級鎖,其步驟以下:
輕量級鎖的釋放:
輕量級鎖的釋放也是經過CAS操做來進行的,主要步驟以下:
對於輕量級鎖,其性能提高的依據是「對於絕大部分的鎖,在整個生命週期內都是不會存在競爭的」,若是打破這個依據則除了互斥的開銷外,還有額外的CAS 操做,所以在有多線程競爭的狀況下,輕量級鎖比重量級鎖更慢。
輕量級鎖好處:
在多線程交替執行同步塊的狀況下,能夠避免重量級鎖引發的性能消耗。
monitor 實現鎖的時候, monitor 會阻塞和喚醒線程,線程的阻塞和喚醒須要CPU 從用戶態轉爲核心態,頻繁的阻塞和喚醒對CPU 來講是一件負擔很重的工做,這些操做給系統的併發性能帶來了很大的壓力。同時,共享數據的鎖定狀態可能只會持續很短的一段時間,爲了這段時間阻塞和喚醒線程並不值得。若是有一個以上的處理器,能讓兩個或以上的線程同時並行執行,就可讓後面請求鎖的那個線程「稍微等一下」,但不放棄處理器的執行時間,看看持有鎖的線程是否釋放了鎖。爲了讓線程等待,咱們只需讓線程執行一個循環(即自旋),這就是自旋鎖。
自旋鎖在JDK 1.4.2中就已經引入,只不過默認是關閉的,可使用-XX:+UseSpinning參數來開啓,在JDK 6中就已經改成默認開啓了。自旋等待不能代替阻塞,且先不說對處理器數量的要求,自旋等待自己雖然避免了線程切換的開銷,但它是要佔用處理器時間的,所以,若是鎖被佔用的時間很短,自旋等待的效果就會很是好,反之,若是鎖被佔用的時間很長。那麼自旋的線程只會白白消耗處理器資源,而不會作任何有用的工做,反而會帶來性能上的浪費。所以,自旋等待的時間必需要有必定的限度,若是自旋超過了限定的次數仍然沒有成功得到鎖,就應當使用傳統的方式去掛起線程了。自旋次數的默認值是10次,用戶可使用參數-XX : PreBlockSpin來更改。
適應性自旋鎖
在JDK 6 中引入了自適應的自旋鎖。若是在同一個鎖對象上,自旋等待剛剛成功得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機就會認爲此次自旋也頗有可能再次成功,進而它將容許自旋等待持續相對更長的時間。若是對於某個鎖,自旋不多成功得到過,那在之後要獲取這個鎖時將可能省略掉自旋過程,以免浪費處理器資源。
減小synchronized的範圍:
同步代碼塊中儘可能短,減小同步代碼塊中代碼的執行時間,減小鎖的競爭。
synchronized(Demo01.class){ System.out.println("aaa"); }
下降synchronized鎖的粒度:
將一個鎖拆分爲多個鎖提升併發度,如HashTable:鎖定整個哈希表,一個操做正在進行時,其餘操做也同時鎖定,效率低下。ConcurrentHashMap:局部鎖定,只鎖定桶。
讀寫分離:
讀取時不加鎖,寫入和刪除時加鎖
ConcurrentHashMap,CopyOnWriteArrayList和ConyOnWriteSet