2 鎖和被保護的對象是不是同一層面
梳理鎖和要保護的對象是否是同一層面的。
案例
- 累加counter
- 測試
- 因為傳參運作100萬次,是以執行後應該輸出100萬,但輸出:
- why?
在非靜态的wrong方法上加鎖,隻能確定多線程無法執行同一執行個體的wrong,無法保證不執行不同執行個體的wrong。靜态counter在多執行個體是共享的,是以會出現線程安全問題。
解決方案
在類中定義一個Object類型的靜态字段,在操作counter之前對該字段加鎖。
評論裡肯定又有人會說:就這?直接把wrong定義為靜态不就行?鎖不就是類級别的了?
是可以,但不可能為解決線程安全改變代碼結構,随便把執行個體方法改為靜态方法。
3 加鎖前考慮鎖粒度和業務場景
方法上加
synchronized
加鎖是簡單,但也不能在業務代碼中濫用:
-
沒必要
絕大多數業務代碼是MVC三層架構,資料經過無狀态的
沒必要使用Controller=>Service=>Repository=>DB
保護什麼資料。是以這也是為何很多同學笑評面試造火箭,工作擰螺絲~synchronized
- 大機率降低性能
使用Spring時,預設Controller、Service、Repository都是單例,加synchronized會導緻整個程式幾乎隻能支援單線程,造成極大性能問題。
即使我們确實有一些共享資源需要保護,也要盡可能減小鎖粒度。就像 concurrentHashMap 的一生發展。
業務代碼有個ArrayList會被多線程操作而需保護,但又有段比較耗時的不涉及線程安全的操作,應該如何加鎖?
推薦隻在操作ArrayList時給這ArrayList加鎖。
正确加鎖的版本幾乎是對錯誤加鎖的十倍性能。
細化鎖後,性能還無法滿足,就要考慮另一個次元的粒度問題:區分讀寫場景以及資源的通路沖突,考慮
4 悲觀鎖 V.S 樂觀鎖
一般業務代碼很少需要進一步考慮這兩種更細粒度的鎖,自己結合業務的性能需求考慮是否要繼續優化:
- 讀寫差異明顯場景,考慮使用
讀寫鎖ReentrantReadWriteLock
- 若JDK版本>8、共享資源的沖突機率也沒那麼大,考慮使用
樂觀讀StampedLock
- JDK的
、ReentrantLock
都提供了公平鎖版本,在沒有明确需求情況下不要輕易開啟公平鎖,在任務很輕情況下開啟公平鎖可能會讓性能下降百倍ReentrantReadWriteLock
5 死鎖
鎖的粒度夠用就好,這意味着程式邏輯中有時會存在一些細粒度鎖。但一個業務邏輯如果涉及多鎖,就很容易産生死鎖。
在電商場景的下單流程中,需要鎖定訂單中多個商品的庫存,拿到所有商品的鎖後再進行下單扣減庫存,全部操作完成後釋放所有鎖。
上線後發現,下單失敗機率高,失敗後使用者需重新下單,極大影響使用者體驗。
案發原因
因為扣減庫存的順序不同,導緻并發下多個線程可能互相持有部分商品的鎖,又等待其他線程釋放另一部分商品的鎖,于是出現死鎖。
接下來,我們剖析一下核心的業務代碼。
首先,定義一個商品類型,包含商品名、庫存剩餘和商品的庫存鎖三個屬性,每一種商品預設庫存1000個;然後,初始化10個這樣的商品對象來模拟商品清單:
模拟在購物車進行商品選購,每次從商品清單(items字段)中随機選購三個商品(不考慮每次選購多個同類商品的邏輯,購物車中不展現商品數量):