Java各種鎖在工作中使用場(chǎng)景和細(xì)節(jié)經(jīng)驗(yàn)總結(jié)
1、synchronized
synchronized 是可重入的排它鎖,和 ReentrantLock 鎖功能相似,任何使用 synchronized 的地方,幾乎都可以使用 ReentrantLock 來(lái)代替,兩者最大的相似點(diǎn)就是:可重入 + 排它鎖,兩者的區(qū)別主要有這些:
- ReentrantLock 的功能更加豐富,比如提供了 Condition,可以打斷的加鎖 API、能滿足鎖 + 隊(duì)列的復(fù)雜場(chǎng)景等等;
- ReentrantLock 有公平鎖和非公平鎖之分,而 synchronized 都是非公平鎖;
- 兩者的使用姿勢(shì)也不同,ReentrantLock 需要申明,有加鎖和釋放鎖的 API,而 synchronized 會(huì)自動(dòng)對(duì)代碼塊進(jìn)行加鎖釋放鎖的操作,synchronized 使用起來(lái)更加方便。
synchronized 和 ReentrantLock 功能相近,所以我們就以 synchronized 舉例。
1.1、共享資源初始化
在分布式的系統(tǒng)中,我們喜歡把一些死的配置資源在項(xiàng)目啟動(dòng)的時(shí)候加鎖到 JVM 內(nèi)存里面去,這樣請(qǐng)求在拿這些共享配置資源時(shí),就可直接從內(nèi)存里面拿,不必每次都從數(shù)據(jù)庫(kù)中拿,減少了時(shí)間開銷。
一般這樣的共享資源有:死的業(yè)務(wù)流程配置 + 死的業(yè)務(wù)規(guī)則配置。
共享資源初始化的步驟一般為:項(xiàng)目啟動(dòng) -> 觸發(fā)初始化動(dòng)作 ->單線程從數(shù)據(jù)庫(kù)中撈取數(shù)據(jù) -> 組裝成我們需要的數(shù)據(jù)結(jié)構(gòu) -> 放到 JVM 內(nèi)存中。
在項(xiàng)目啟動(dòng)時(shí),為了防止共享資源被多次加載,我們往往會(huì)加上排它鎖,讓一個(gè)線程加載共享資源完成之后,另外一個(gè)線程才能繼續(xù)加載,此時(shí)的排它鎖我們可以選擇 synchronized 或者 ReentrantLock,我們以 synchronized 為例,寫了 mock 的代碼,如下:
// 共享資源 private static final Map<String, String> SHARED_MAP = Maps.newConcurrentMap(); // 有無(wú)初始化完成的標(biāo)志位 private static boolean loaded = false; /** * 初始化共享資源 */ @PostConstruct public void init(){ if(loaded){ return; } synchronized (this){ // 再次 check if(loaded){ return; } log.info("SynchronizedDemo init begin"); // 從數(shù)據(jù)庫(kù)中撈取數(shù)據(jù),組裝成 SHARED_MAP 的數(shù)據(jù)格式 loaded = true; log.info("SynchronizedDemo init end"); } }
不知道大家有沒(méi)有從上述代碼中發(fā)現(xiàn) @PostConstruct 注解,@PostConstruct 注解的作用是在 Spring 容器初始化時(shí),再執(zhí)行該注解打上的方法,也就是說(shuō)上圖說(shuō)的 init 方法觸發(fā)的時(shí)機(jī),是在 Spring 容器啟動(dòng)的時(shí)候。
大家可以下載演示代碼,找到 DemoApplication 啟動(dòng)文件,在 DemoApplication 文件上右擊 run,就可以啟動(dòng)整個(gè) Spring Boot 項(xiàng)目,在 init 方法上打上斷點(diǎn)就可以調(diào)試了。
我們?cè)诖a中使用了 synchronized 來(lái)保證同一時(shí)刻,只有一個(gè)線程可以執(zhí)行初始化共享資源的操作,并且我們加了一個(gè)共享資源加載完成的標(biāo)識(shí)位(loaded),來(lái)判斷是否加載完成了,如果加載完成,那么其它加載線程直接返回。
如果把 synchronized 換成 ReentrantLock 也是一樣的實(shí)現(xiàn),只不過(guò)需要顯示的使用 ReentrantLock 的 API 進(jìn)行加鎖和釋放鎖,使用 ReentrantLock 有一點(diǎn)需要注意的是,我們需要在 try 方法塊中加鎖,在 finally 方法塊中釋放鎖,這樣保證即使 try 中加鎖后發(fā)生異常,在 finally 中也可以正確的釋放鎖。
有的同學(xué)可能會(huì)問(wèn),不是可以直接使用了 ConcurrentHashMap 么,為什么還需要加鎖呢?的確 ConcurrentHashMap 是線程安全的,但它只能夠保證 Map 內(nèi)部數(shù)據(jù)操作時(shí)的線程安全,是無(wú)法保證多線程情況下,查詢數(shù)據(jù)庫(kù)并組裝數(shù)據(jù)的整個(gè)動(dòng)作只執(zhí)行一次的,我們加 synchronized 鎖住的是整個(gè)操作,保證整個(gè)操作只執(zhí)行一次。
2、CountDownLatch
2.1、場(chǎng)景
1:小明在淘寶上買了一個(gè)商品,覺(jué)得不好,把這個(gè)商品退掉(商品還沒(méi)有發(fā)貨,只退錢),我們叫做單商品退款,單商品退款在后臺(tái)系統(tǒng)中運(yùn)行時(shí),整體耗時(shí) 30 毫秒。
2:雙 11,小明在淘寶上買了 40 個(gè)商品,生成了同一個(gè)訂單(實(shí)際可能會(huì)生成多個(gè)訂單,為了方便描述,我們說(shuō)成一個(gè)),第二天小明發(fā)現(xiàn)其中 30 個(gè)商品是自己沖動(dòng)消費(fèi)的,需要把 30 個(gè)商品一起退掉。
2.2、實(shí)現(xiàn)
此時(shí)后臺(tái)只有單商品退款的功能,沒(méi)有批量商品退款的功能(30 個(gè)商品一次退我們稱為批量),為了快速實(shí)現(xiàn)這個(gè)功能,同學(xué) A 按照這樣的方案做的:for 循環(huán)調(diào)用 30 次單商品退款的接口,在 qa 環(huán)境測(cè)試的時(shí)候發(fā)現(xiàn),如果要退款 30 個(gè)商品的話,需要耗時(shí):30 * 30 = 900 毫秒,再加上其它的邏輯,退款 30 個(gè)商品差不多需要 1 秒了,這個(gè)耗時(shí)其實(shí)算很久了,當(dāng)時(shí)同學(xué) A 提出了這個(gè)問(wèn)題,希望大家?guī)兔纯慈绾蝺?yōu)化整個(gè)場(chǎng)景的耗時(shí)。
同學(xué) B 當(dāng)時(shí)就提出,你可以使用線程池進(jìn)行執(zhí)行呀,把任務(wù)都提交到線程池里面去,假如機(jī)器的 CPU 是 4 核的,最多同時(shí)能有 4 個(gè)單商品退款可以同時(shí)執(zhí)行,同學(xué) A 覺(jué)得很有道理,于是準(zhǔn)備修改方案,為了便于理解,我們把兩個(gè)方案都畫出來(lái),對(duì)比一下:
同學(xué) A 于是就按照演變的方案去寫代碼了,過(guò)了一天,拋出了一個(gè)問(wèn)題:向線程池提交了 30 個(gè)任務(wù)后,主線程如何等待 30 個(gè)任務(wù)都執(zhí)行完成呢?因?yàn)橹骶€程需要收集 30 個(gè)子任務(wù)的執(zhí)行情況,并匯總返回給前端。
大家可以先不往下看,自己先思考一下,我們前幾章說(shuō)的那種鎖可以幫助解決這個(gè)問(wèn)題?
CountDownLatch 可以的,CountDownLatch 具有這種功能,讓主線程去等待子任務(wù)全部執(zhí)行完成之后才繼續(xù)執(zhí)行。
此時(shí)還有一個(gè)關(guān)鍵,我們需要知道子線程執(zhí)行的結(jié)果,所以我們用 Runnable 作為線程任務(wù)就不行了,因?yàn)?Runnable 是沒(méi)有返回值的,我們需要選擇 Callable 作為任務(wù)。
我們寫了一個(gè) demo,首先我們來(lái)看一下單個(gè)商品退款的代碼:
// 單商品退款,耗時(shí) 30 毫秒,退款成功返回 true,失敗返回 false @Slf4j public class RefundDemo { /** * 根據(jù)商品 ID 進(jìn)行退款 * @param itemId * @return */ public boolean refundByItem(Long itemId) { try { // 線程沉睡 30 毫秒,模擬單個(gè)商品退款過(guò)程 Thread.sleep(30); log.info("refund success,itemId is {}", itemId); return true; } catch (Exception e) { log.error("refundByItemError,itemId is {}", itemId); return false; } } }
接著我們看下 30 個(gè)商品的批量退款,代碼如下:
@Slf4j public class BatchRefundDemo { // 定義線程池 public static final ExecutorService EXECUTOR_SERVICE = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(20)); @Test public void batchRefund() throws InterruptedException { // state 初始化為 30 CountDownLatch countDownLatch = new CountDownLatch(30); RefundDemo refundDemo = new RefundDemo(); // 準(zhǔn)備 30 個(gè)商品 List<Long> items = Lists.newArrayListWithCapacity(30); for (int i = 0; i < 30; i++) { items.add(Long.valueOf(i+"")); } // 準(zhǔn)備開始批量退款 List<Future> futures = Lists.newArrayListWithCapacity(30); for (Long item : items) { // 使用 Callable,因?yàn)槲覀冃枰鹊椒祷刂? Future<Boolean> future = EXECUTOR_SERVICE.submit(new Callable<Boolean>() { @Override public Boolean call() throws Exception { boolean result = refundDemo.refundByItem(item); // 每個(gè)子線程都會(huì)執(zhí)行 countDown,使 state -1 ,但只有最后一個(gè)才能真的喚醒主線程 countDownLatch.countDown(); return result; } }); // 收集批量退款的結(jié)果 futures.add(future); } log.info("30 個(gè)商品已經(jīng)在退款中"); // 使主線程阻塞,一直等待 30 個(gè)商品都退款完成,才能繼續(xù)執(zhí)行 countDownLatch.await(); log.info("30 個(gè)商品已經(jīng)退款完成"); // 拿到所有結(jié)果進(jìn)行分析 List<Boolean> result = futures.stream().map(fu-> { try { // get 的超時(shí)時(shí)間設(shè)置的是 1 毫秒,是為了說(shuō)明此時(shí)所有的子線程都已經(jīng)執(zhí)行完成了 return (Boolean) fu.get(1,TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); } return false; }).collect(Collectors.toList()); // 打印結(jié)果統(tǒng)計(jì) long success = result.stream().filter(r->r.equals(true)).count(); log.info("執(zhí)行結(jié)果成功{},失敗{}",success,result.size()-success); } }
上述代碼只是大概的底層思路,真實(shí)的項(xiàng)目會(huì)在此思路之上加上請(qǐng)求分組,超時(shí)打斷等等優(yōu)化措施。
我們來(lái)看一下執(zhí)行的結(jié)果:
從執(zhí)行的截圖中,我們可以明顯的看到 CountDownLatch 已經(jīng)發(fā)揮出了作用,主線程會(huì)一直等到 30 個(gè)商品的退款結(jié)果之后才會(huì)繼續(xù)執(zhí)行。
接著我們做了一個(gè)不嚴(yán)謹(jǐn)?shù)膶?shí)驗(yàn)(把以上代碼執(zhí)行很多次,求耗時(shí)平均值),通過(guò)以上代碼,30 個(gè)商品退款完成之后,整體耗時(shí)大概在 200 毫秒左右。
而通過(guò) for 循環(huán)單商品進(jìn)行退款,大概耗時(shí)在 1 秒左右,前后性能相差 5 倍左右,for 循環(huán)退款的代碼如下:
long begin1 = System.currentTimeMillis(); for (Long item : items) { refundDemo.refundByItem(item); } log.info("for 循環(huán)單個(gè)退款耗時(shí){}",System.currentTimeMillis()-begin1);
性能的巨大提升是線程池 + 鎖兩者結(jié)合的功勞。
3、總結(jié)
本章舉了實(shí)際工作中的兩個(gè)小案列,看到了 CountDownLatch 和 synchronized(ReentrantLock) 是如何結(jié)合實(shí)際需求進(jìn)行落地的,特別是 CountDownLatch 的案列,使用線程池 + 鎖結(jié)合的方式大大提高了生產(chǎn)效率,所以在工作中如果你也遇到相似的場(chǎng)景,可以毫不猶豫地用起來(lái)。
以上就是Java各種鎖在工作中使用場(chǎng)景和細(xì)節(jié)經(jīng)驗(yàn)總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于Java鎖在工作中使用場(chǎng)景細(xì)節(jié)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java C++ leetcode執(zhí)行一次字符串交換能否使兩個(gè)字符串相等
這篇文章主要為大家介紹了Java C++ leetcode1790執(zhí)行一次字符串交換能否使兩個(gè)字符串相等,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10IDEA 非常重要的一些設(shè)置項(xiàng)(一連串的問(wèn)題差點(diǎn)讓我重新用回 Eclipse)
這篇文章主要介紹了IDEA 非常重要的一些設(shè)置項(xiàng)(一連串的問(wèn)題差點(diǎn)讓我重新用回 Eclipse),本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08SpringIOC框架的簡(jiǎn)單實(shí)現(xiàn)步驟
這篇文章主要介紹了SpringIOC框架簡(jiǎn)單實(shí)現(xiàn)步驟,幫助大家更好的理解和學(xué)習(xí)使用Spring,感興趣的朋友可以了解下2021-05-05spring?和?idea?建議不要使用?@Autowired注解的原因解析
@Autowired?是Spring框架的注解,而@Resource是JavaEE的注解,這篇文章主要介紹了spring和idea建議不要使用@Autowired注解的相關(guān)知識(shí),需要的朋友可以參考下2023-11-11使用IDEA和Gradle構(gòu)建Vertx項(xiàng)目(圖文步驟)
這篇文章主要介紹了使用IDEA和Gradle構(gòu)建Vertx項(xiàng)目(圖文步驟),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-09-09springboot配置多個(gè)數(shù)據(jù)源兩種方式實(shí)現(xiàn)
在我們的實(shí)際業(yè)務(wù)中可能會(huì)遇到;在一個(gè)項(xiàng)目里面讀取多個(gè)數(shù)據(jù)庫(kù)的數(shù)據(jù)來(lái)進(jìn)行展示,spring對(duì)同時(shí)配置多個(gè)數(shù)據(jù)源是支持的,本文主要介紹了springboot配置多個(gè)數(shù)據(jù)源兩種方式實(shí)現(xiàn),感興趣的可以了解一下2022-03-03java啟動(dòng)jar包將日志打印到文本的簡(jiǎn)單操作
這篇文章主要介紹了java啟動(dòng)jar包將日志打印到文本的簡(jiǎn)單操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08Java基于Google zxing生成帶logo的二維碼圖片
zxing是一個(gè)開放源碼的,用java實(shí)現(xiàn)的多種格式的1D/2D條碼圖像處理庫(kù),本文主要介紹了Java基于Google zxing生成帶logo的二維碼圖片,具有一定的參考價(jià)值,感興趣的可以了解一下2023-10-10將Swagger2文檔導(dǎo)出為HTML或markdown等格式離線閱讀解析
這篇文章主要介紹了將Swagger2文檔導(dǎo)出為HTML或markdown等格式離線閱讀,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-11-11