1.鎖策略
1.1 樂觀鎖與悲觀鎖
其實前三個鎖是同一種鎖,只是站在不同的角度上去進行描述,此處的樂觀與悲觀其實是指在預測的角度上看會發生鎖競爭的概率大小,概率大的則是悲觀鎖,概率小的則是樂觀鎖
樂觀鎖在加鎖的時候就會做較少的事情,加鎖的速度較快,但是消耗的cpu資源等也會增加,悲觀鎖在加鎖的時候就會做很多事情來避免鎖的沖突,從而加鎖的時候做的事情就比較多,加鎖的開銷相對較小
1.2 輕量級鎖與重量級鎖
這里是從加鎖的量級出發,本質上是和上面的悲觀鎖和樂觀鎖的意思差不多,意思是從結果的角度上看,輕量級的鎖加鎖開銷小,重量級的鎖加鎖開銷比較大
1.3 自旋鎖與掛起等待鎖
自旋鎖是輕量級鎖和樂觀鎖的一種典型實現方式,掛起等待鎖是重量級鎖的一種典型實現方式,自旋鎖就是有一個while循環來一直判斷是否需要加鎖,加上鎖了就跳出循環,加鎖不成功就繼續判斷而不是阻塞,這就導致了一直消耗了系統的資源而沒有做實事,所以說消耗了相對多的cpu資源
掛起等待鎖則與他截然相反,掛起等待鎖就需要內核調度器去參與操作了,所以要做的事情也就多了,所以需要獲取到鎖的時間也就多了
1.4 普通互斥鎖與讀寫鎖
普通互斥鎖類似于Synchronized,有加鎖和解鎖兩個動作
讀寫鎖則是兩種鎖
讀鎖和讀鎖之間不會有鎖沖突
寫鎖和讀鎖之間會有鎖沖突
寫鎖之間也會有鎖沖突
總結:一個線程加讀鎖的時候,另一個線程只能讀不能寫
一個線程加寫鎖的時候,另一個線程不能寫也不能讀
這個要和MySQL中事務的隔離級別要分得開
MySQL中的臟讀,不可重復讀,幻讀
我們以一個有一條數據的表為例,age=10
臟讀:事務A修改age =20,沒有提交事務,事務B讀取到這條數據之后,事務A回滾了,此時B就讀到了一條臟的數據
不可重復讀:事務B讀取到了age=10,此時事務A修改數據為20并提交事務,事務B又一次查詢數據,查到的兩條數據是不一樣的,這就是不可重復讀
幻讀:事務B查詢到一條數據,此時事務A又增加一條數據,此時事務B再次查詢就是兩條數據,這就稱為幻讀
不可重復讀和臟讀之間的區別是:不可重復讀讀取的事務是已經提交的
1.5 公平鎖與非公平鎖
與之前介紹的線程餓死有一定的聯系
這里的公平指的是先到先得,只要線程釋放鎖,第一個等待的線程可以率先拿到鎖,就不會出現持有鎖,釋放鎖,這樣的循環持有的狀態,導致其他線程沒辦法做事情
這就需要引入一個數據結構來實現先到先得這種特點
java原生的鎖其實就是非公平鎖,靠搶占式執行.
1.6 可重入鎖與不可重入鎖
我們之前說過,Synchronized就是一個可重入鎖,就是針對一個線程,不斷用這個鎖加鎖很多次,可重入鎖不會出現問題,因為可重入鎖只是增加了計數器,實際上仍然是只加了一層鎖結構,而可重入鎖就可能出現死鎖的情況
2.Synchronized的鎖優化策略
我們都說Synchronized有自適應的效果,能夠根據鎖沖突狀態確定自己是什么類型的鎖,那么到底是怎么回事呢??咱們慢慢說
Synchronized鎖其實有三種狀態,根據情況來逐級遞增,注意這里的鎖級別是不可降級的
1.偏向鎖狀態(假設沒線程來競爭鎖)
這里的核心思想就是懶漢模式的思想,用的時候再創建,能不加鎖就不加鎖,所謂的偏向鎖,就是給線程加上一個非常輕量級的標記,如果沒有人來競爭鎖,就直接省略這樣的加鎖的操作,有的話則升級為輕量鎖
2.輕量級鎖狀態(假設競爭較小)
此處的實現就是自旋鎖,優點是可以第一時間拿到鎖,缺點是比較消耗cpu資源
與此同時Synchronized也會計算鎖競爭的激烈程度,從而來判斷是否需要升級到重量級鎖
對于輕量級鎖來說,假設競爭這個鎖的線程很多,那么大多數線程此刻就處自旋的狀態,此刻就比較消耗cpu資源
3.重量級鎖狀態(假設競爭很大)
此時拿不到鎖的線程就不會選擇去自旋了,而是直接阻塞等待,直接讓出cpu,當線程釋放之后就會隨機分配一個線程來持有這個鎖
4.鎖消除策略
Synchronized會自動判斷線程加上的鎖是否有效,在編譯期間,如果判斷無效就會自動把這個鎖給干掉,比如說這里沒有涉及到多個線程對成員變量的修改等等
5.鎖粗化
會將多個細粒度的鎖,合并成一個粗粒度的鎖,避免了重復加鎖解鎖的過程
3.CAS策略 (避免使用鎖的另一種解決線程安全問題的策略)
全稱叫做compare and swap 就是比較和交換,其實是一個cpu指令
關于CAS的api都放在java的unsafe包內,也就是暫時不推薦去使用的
我們簡單介紹一下它的觀點與使用技巧
這里給出一段偽代碼
address:內存中的地址
expectValue:寄存器1中的值
swapvalue:寄存器2中的值
比較內存中的值和寄存器1的值是否相同,相同則直接交換(賦值),返回一個true不相同則無事發生,返回一個false
java標準庫也提供了很多原子類,保證了多線程操作一個數據是原子的,本質上就是基于cas的
我們這個時候用兩個線程給她進行自增50000次就會獲取到正確的結果,而不會出現線程安全問題了
原始標準庫里的代碼略顯復雜,這里我們使用簡化版本的進行說明
這里的一次自增操作是先拿到舊數據,然后使用cas進行操作,如果判斷相等則直接寫入內存,不相等則更新一下目前的舊值為內存中的最新值,從而進行自增,這里就不會出現線程安全問題了
如有問題,希望大家多多指正?