咱們都清楚當多個線程去同時作一件事情的時候,咱們須要考慮原子性、可見性、和有序性這幾個問題,本章主要說原子性,如下是闡述內容java
- 原子性:主要用原子性問題進行展開討論
- 同步鎖(synchronize):使用同步鎖解決問題
- MarkWord對象頭:鎖的狀態存在哪裏
- synchronize的鎖升級機制:多個線程搶佔資源的時候,鎖的底層狀態是如何改變的
- CAS機制:當無鎖化時候(例如自旋鎖的時候,cas起到的做用)
【原子性】:指的是一個操做一旦進行就不能被別的操做進行干擾。咱們建立兩個線程對一個數字進行增長,兩個線程都增長數字10000,按照常理來說,結果應該是20000,實則否則數據庫
public class AtomicDemo { private int i =0; private void mock(){ i++; } public static void main(String[] args) throws InterruptedException { AtomicDemo atomicDemo=new AtomicDemo(); Thread[] thread =new Thread[2]; for (int i = 0; i < thread.length; i++) { thread[i]=new Thread(()->{ for (int j = 0; j <10000 ; j++) { atomicDemo.mock(); } }); thread[i].start(); } thread[0].join(); thread[1].join(); System.out.println(atomicDemo.i); } }
以上述demo爲例,實際上當線程對 i 進行 ++ 的時候,在底層分三步編程
- 加載 內存中 i
- 對i++
- 把++後的數據寫入內存
那咱們能夠想象一下,當線程a正在對i++併發
- ->此時線程cpu切換到線程B
- ->當線程b把i=0變成i=1
- ->這個時候cpu又切換到線程a
- ->那麼線程a按照以前執行的位置再次進行相加,可是以前的位置是i=0,因此兩個線程都循環了一次結果卻只是相加了1
- 這就是爲何最終結果不等於20000 的緣由了
進行驗證:打開terminal 輸入 [javap -v AtomicDemo.class] 查看字節指令工具
因此頗有可能在某個過程當中被別的線程打斷,從而獲得咱們預期外的結果佈局
實質上有不少方法解決,咱們今天只圍繞synchronized進行展開,咱們只用給這個方法加上synchronized就能夠了。性能
synchronize就是一種排他鎖,換句話說是一種互斥鎖,他能夠同一時間只讓一個線程對你的邏輯進行訪問,那麼我咱們的邏輯受到怎麼樣的保護,或者說這個關鍵字的範圍是什麼呢?如何使用呢?優化
能夠修飾:實例方法(對象範圍)、代碼塊(取決於括號中你所放置的內容this/對象.class)、靜態方法(對象範圍)this
實例方法:atom
若是兩個線程同時訪問不一樣對象的同一個方法,那麼他們不是互斥關係,就是這個關鍵字不會保護你的邏輯
若是訪問的是同一個對象,則會對你的邏輯進行保護,好比你的method1中寫了一個邏輯,必須等線程1執行完成線程而才能執行
代碼塊:若是你的括號中寫的是this那和實例方法的做用域是同樣的
可是若是括號中寫的是**.class那做用域就是無論多少線程想執行必須一個一個進行排隊
靜態方法(由於靜態方法是一個類產生就產生的,因此他的做用也是全局的)
tips: 細細想來,實際上是對象決定synchronized 的範圍,咱們想一想,若是有多個線程搶佔同一個資源,那其中一個搶佔到怎麼通知別的線程這個坑位已經被搶佔了呢?是否有一個全局的位置去存儲鎖的標記呢,那既然對象決定關鍵字的範圍,那麼一個資源是否被搶佔到,【是否搶佔標記】是否會存儲在每一個對象中呢?對的,如今就來講一下MarkWord對象頭
對象在內存中的佈局分爲:
- 對象頭(Header):這一部分由兩部分組成
- Mark Word:這一部分就是存儲鎖的標記
- class 對象指針(類元信息):對象對應的原數據對象的內存地址
- 實例數據(Instance Data):這裏存儲的就是對象的成員變量等
對齊填充(Padding):這一部分屬於一個優化部分,因爲HotSpot VM的自動內存管理要求對象起始地址必須是8字節的整數倍,因此若是不是8的倍數,那就就會自動填充爲8的倍數,不然進行讀取就會消耗多餘的性能,僅此而已
tips: 由下圖能夠看出,當線程對資源進行獲取的時候,看一下這個鎖的狀態,在決定是否進行搶佔,這個是否能夠搶佔在你規定的對象中存儲,這也就是爲何線程對兩個不一樣的對象的資源進行搶佔時,其資源不受保護的緣由了,(由於他們有不一樣的鎖的狀態)
咱們來看看是否鎖的狀態被存儲了起來
增長這個jar,這是用來查詢對象的大小和佈局的工具,由 openjdk 提供
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>咱們用這張圖片做爲參考去查詢鎖的狀態
對對象的佈局進行打印
結果以下:
tip:咱們已經知道了鎖的狀態被存儲起來,那麼那些輕量級、重量級、等等,鎖的狀態都表明了什麼呢,鎖的狀態是怎麼流轉的呢,來讓咱們繼續剖析,以及來模擬狀態而且看一下狀態markword中的狀態是否變了...
【在jdk1.6以前只有無鎖->重量級鎖,使用‘synchronize’的流程是,若是一個線程沒有搶佔到資源,那就直接是一個重量級鎖的狀態,爲了提升性能,纔有了後面的鎖的狀態】
synchronize鎖的類型有這幾種:
- 無鎖:
- 偏向鎖:就是當沒有線程對資源競爭的時候,
- 此時線程a獲得了資源,
- 那這個鎖就會存儲一個線程a的指向地址,
- 下次線程a再次進入資源,就不須要搶佔鎖,直接能夠進行執行
- 輕量級鎖:避免線程阻塞(由於如何線程阻塞,咱們再次對線程進行喚醒,那就增長了性能開銷,也就是從用戶態到內核態) -》從偏向鎖升級而來
- 線程b也來搶佔資源,可是發現鎖已經偏向了線程a,那麼他就要升級爲輕量級鎖,輕量級鎖,使用自旋鎖進行實現
- 自旋鎖:實際上就是用一個for循環不斷詢問時候別的線程已經執行完成,當前正在搶佔線程的線程就能夠執行,然而在循環的時候確定會牽扯到線程問題,那麼咱們用CAS進行實現
- 重量級鎖:比較消耗性能,爲了優化,因此在1.6後引入了偏向鎖和輕量級鎖
- 牽扯用戶態到內核態的交換(用戶態就是在java中的操做,而內核態就牽扯到cpu層的操做,因此更消耗性能),
- 沒有得到鎖的線程會阻塞,當這個線程得到鎖的時候在進行喚醒
public class LockDemo { Object o=new Object(); public static void main(String[] args) { LockDemo lockDemo=new LockDemo(); System.out.println(ClassLayout.parseInstance(lockDemo).toPrintable()); // 加鎖後的狀態 System.out.println("加鎖以後--------"); synchronized (lockDemo){ System.out.println(ClassLayout.parseInstance(lockDemo).toPrintable()); } } }咱們觀察markword中的狀態,參考查詢鎖的狀態那張圖
咱們發現,按照上面的畫的流程來說,應該首先是無鎖,以後是偏向鎖啊,爲何這裏是自旋鎖的?由於偏向鎖是默認關閉的,由於在程序啓動的時候已經有一些咱們不知道的線程在底層運行了,那麼下個線程來的話直接就會執行重量級鎖了。
public class HeightLockDemo { public static void main(String[] args) { HeightLockDemo heightLockDemo = new HeightLockDemo(); Thread thread = new Thread(() -> { synchronized (heightLockDemo) { System.out.println("線程1"); System.out.println(ClassLayout.parseInstance(heightLockDemo).toPrintable()); } }); thread.start(); synchronized (heightLockDemo) { System.out.println("主線程"); System.out.println(ClassLayout.parseInstance(heightLockDemo).toPrintable()); } } }
【CAS(Compare And Set)】:其實就是在操做邏輯以前,拿以前的數值,和你的預期 值進行對比,若是相同那就修改你傳入的新的數值。實際上有點像數據庫的樂觀鎖
那在輕量級鎖中大概的流程就是這樣的:首先不斷循環->判斷cas,若是cas返回ture則對鎖的狀態進行修改,固然除了cas的判斷外能夠進行breadk,確定還有自旋次數的限制,不然循環就無終止了
在java中有使用cas的例子【sun.misc.Unsafe#compareAndSwapObject】
那麼CAS的底層又是如何實現的,由於在底層也是多個線程來搶佔這個CAS的,其實CAS的底層也是用鎖來實現的,只不過是CPU層面的鎖。