互聯(lián)網(wǎng)行業(yè)分為哪幾類排名優(yōu)化方法
常見的鎖策略
此處談到的鎖策略,不局限于 Java,C++,Python,數(shù)據(jù)庫,操作系統(tǒng)……但凡是涉及到鎖,都是可以應用到下列的鎖策略的
樂觀鎖 vs 悲觀鎖
鎖的實現(xiàn)者,預測接下來鎖沖突(鎖競爭,兩個線程針對一個對象加鎖,產(chǎn)生阻塞等待了)的概率是大,還是不大,根據(jù)這個沖突的概率,來接下來做什么
~~ 這不是兩把具體的鎖,而是“兩類鎖”.
悲觀鎖:預測鎖競爭不是很激烈.
樂觀鎖:預測鎖競爭會很激烈.
通常來說,悲觀鎖一般做的工作更多一些,效率更低一些,樂觀鎖做的工作會更少一些,效率更高一點.但是并不絕對.
悲觀鎖和樂觀鎖,唯一的區(qū)別,主要就是看預測鎖競爭激烈程度的結論.
輕量級鎖 vs 重量級鎖
輕量級鎖: 加鎖解鎖,過程更快更高效.
重量級鎖: 加鎖解鎖,過程更慢,更低效.
一個樂觀鎖很可能也是一個輕量級鎖,一個悲觀鎖很可能也是一個重量級鎖.
多數(shù)情況下,樂觀鎖,也是一個輕量級鎖.
多數(shù)情況下,悲觀鎖也是一個重量級鎖.
自旋鎖 vs 掛起等待鎖
自旋鎖是輕量級鎖的一種典型實現(xiàn).
掛起等待鎖是重量級鎖的一種典型實現(xiàn).
互斥鎖 vs 讀寫鎖
互斥鎖
synchronized,是互斥鎖
synchronized 只有兩個操作:
1.進入代碼塊,加鎖
2.出了代碼塊,解鎖
加鎖,就只是單純的加鎖,沒有更細化的區(qū)分了
讀寫鎖
~~ 讀寫鎖,能夠把讀和寫兩種加鎖區(qū)分開
讀寫鎖:
1.給讀加鎖
2.給寫加鎖
3.解鎖
注: 如果多個線程讀同一個變量,不會涉及到線程安全問題!!!
讀寫鎖中,約定:
1.讀鎖和讀鎖之間,不會鎖競爭.不會產(chǎn)生阻塞等待,不會影響程序的速度,代碼執(zhí)行很快.
2.寫鎖和寫鎖之間,有鎖競爭,減慢速度,但是保證準確性.
3.讀鎖和寫鎖之間,也有鎖競爭,減慢速度,但是保證準確性.
注:
1.非必要不加鎖.
2.讀寫鎖更適合于,一寫多讀的情況.
3.多線程針對同一個變量并發(fā)讀,這時是沒有線程安全問題的,也就不需要加鎖控制.
4.很多開發(fā)場景中,讀操作非常高頻,比寫操作的頻率高很多.
5.在Java標準庫里面也提供了讀寫鎖的具體實現(xiàn)(兩個類,讀鎖類,寫鎖類).
公平鎖 vs 非公平鎖
此處把公平定義成“先來后到”
公平鎖: 當女神分手之后,就由等待隊列中,最早來的舔狗上位.
非公平鎖: 雨露均沾了.
注: 操作系統(tǒng)和 Java synchronized 原生都是“非公平鎖”,操作系統(tǒng)這里的針對加鎖的控制,本身就依賴于線程調度順序.這個調度順序是隨機的,不會考慮到這個線程等待鎖多久了.
要想實現(xiàn)公平鎖,就得在這個基礎上,能夠引入額外的東西(引入一個隊列,讓這些加鎖的線程去排隊).
可重入鎖 vs 不可重入鎖
不可重入鎖: 一個線程針對一把鎖,連續(xù)加鎖兩次,出現(xiàn)死鎖.
可重入鎖: 一個線程針對一把鎖,連續(xù)加鎖多次都不會死鎖.
注: 系統(tǒng)原生的鎖,C++標準庫的鎖,Python標準庫的鎖…都不是可重入的鎖!
synchronized是個"可重入鎖",(加鎖的時候會判定一下,看當前嘗試申請鎖的線程是不是已經(jīng)就是鎖的擁有者了,如果是,直接放行)
synchronized鎖
針對上述六組鎖策略, synchronized這把鎖屬于哪種呢??
synchronized 既是悲觀鎖,也是樂觀鎖 ~~ synchronized會根據(jù)當前鎖競爭的激烈程度,自適應.
既是輕量級鎖,也是重量級鎖 ~~ synchronized默認是輕量級鎖,如果發(fā)現(xiàn)當前鎖競爭比較激烈,就會轉換成重量級鎖.
synchronized這里的輕量級鎖部分基于自旋鎖的方式實現(xiàn),synchronized這里的重量級鎖部分基于掛起等待鎖的方式實現(xiàn).
synchronized不是讀寫鎖.
synchronized是非公平鎖.
synchronized是可重入鎖.
總結: 上述談到的六種鎖策略,可以視為是“鎖的形容詞”.
CAS
CAS ~~ 全稱Compare and swap, 字面意思:”比較并交換“
一個 CAS 涉及到以下操作 :
我們假設內存中的原數(shù)據(jù)V,舊的預期值A,需要修改的新值B。
1.比較 A 與 V 是否相等。(比較)
2.如果比較相等,將 B 寫入 V。(交換)
3.返回操作是否成功
此處最特別的地方,上述這個 CAS 的過程,并非是通過一段代碼實現(xiàn)的,而是通過一條 CPU 指令完成的 => CAS 操作是原子的 ~~ 就可以在一定程度上回避線程安全問題
因此解決線程安全問題除了加鎖之外,又有了一個新的方向了.
小結: CAS 可以理解成是 CPU 給我們提供的一個特殊指令,通過這個指令,就可以一定程度的處理線程安全問題.
CAS 偽代碼
注: 下面寫的代碼不是原子的, 真實的 CAS 是一個原子的硬件指令完成的. 這個偽代碼只是輔助理解 CAS 的工作流程.
CAS 的應用場景
1.實現(xiàn)原子類(Java 標準庫里提供的類)
標準庫中提供了
java.util.concurrent.atomic
包, 里面的類都是基于這種方式來實現(xiàn)的.典型的就是AtomicInteger
類,其中的getAndIncrement
相當于i++
操作.
import java.util.concurrent.atomic.AtomicInteger;/*** Created with IntelliJ IDEA.* Description:* User: fly(逐夢者)* Date: 2023-10-09* Time: 10:49*/
public class ThreadDemo28 {public static void main(String[] args) throws InterruptedException {// 這些原子類,就是基于 CAS 實現(xiàn)了 自增,自減等操作.此時進行這類操作不需要加鎖,也是線程安全的.AtomicInteger count = new AtomicInteger(0);// 使用原子類, 來解決線程安全問題Thread t1 = new Thread(() -> {for (int i = 0; i < 5_0000; i++) {// 因為 java 不支持運算符重載,所以只能使用普通方法來表示自增自減count.getAndIncrement();// count++//count.incrementAndGet(); => ++ count//count.getAndDecrement(); => count--//count.decrementAndGet(); => -- count}});Thread t2 = new Thread(() -> {for (int i = 0; i < 5_0000; i++) {count.getAndIncrement();}});t1.start();t2.start();t1.join();t2.join();System.out.println(count.get());}
}
偽代碼實現(xiàn)AtomicInteger
2.實現(xiàn)自旋鎖
自旋鎖偽代碼
注: CAS 屬于“特殊方法”,synchronized 屬于“通用方法” –> 各種場景,都能使用(打擊面廣)
CAS 的典型問題: ABA 問題
CAS 在運行中的核心,檢查 value 和 oldValue 是否一致.如果一致,就視為value 中途沒有被修改過,所以進行下一步交換操作是沒問題的.
這里一致,可能是沒改過.也可能是,改過,但是還原回來了?!
把 value 的值設為A的話, CAS 判定 value 為 A,此時可能確實value始終是A,也可能是value本來是A,被改成了B,又還原成了A …
ABA 問題就相當于,買個手機,買到的這個手機,可能是新機,也可能是翻新機.
翻新機: 二手的,被銷售商回收了,經(jīng)過一些翻新操作(把外殼換了,重新包裝).
ABA 這個情況,大部分情況下,其實是不會對代碼/邏輯產(chǎn)生太大影響的,但是不排除一些“極端情況”,也是可能造成影響的.
例子:
上述場景,概率非常低!!!一方面,恰好,滑稽這邊多按了幾次,產(chǎn)生多個扣款動作了,另一方面,恰好在這個非常極限的時間內,有人轉賬了一樣的金額.
解決方案
針對當前問題,采取的方案,就是加入一個版本號.想象成,初始版本號是1,每次修改版本號都+1,然后進行 CAS 的時候不是以金額為基準了,而是以版本號為基準.此時,版本號要是沒變,就是一定沒有發(fā)生改變(版本號是只能增長,不能降低的).