Java編程實(shí)現(xiàn)排他鎖代碼詳解
一 .前言
某年某月某天,同事說需要一個文件排他鎖功能,需求如下:
(1)寫操作是排他屬性
(2)適用于同一進(jìn)程的多線程/也適用于多進(jìn)程的排他操作
(3)容錯性:獲得鎖的進(jìn)程若Crash,不影響到后續(xù)進(jìn)程的正常獲取鎖
二 .解決方案
1. 最初的構(gòu)想
在Java領(lǐng)域,同進(jìn)程的多線程排他實(shí)現(xiàn)還是較簡易的。比如使用線程同步變量標(biāo)示是否已鎖狀態(tài)便可。但不同進(jìn)程的排他實(shí)現(xiàn)就比較繁瑣。使用已有API,自然想到 java.nio.channels.FileLock:如下
/** * @param file * @param strToWrite * @param append * @param lockTime 以毫秒為單位,該值只是方便模擬排他鎖時使用,-1表示不考慮該字段 * @return */ public static boolean lockAndWrite(File file, String strToWrite, boolean append,int lockTime){ if(!file.exists()){ return false; } RandomAccessFile fis = null; FileChannel fileChannel = null; FileLock fl = null; long tsBegin = System.currentTimeMillis(); try { fis = new RandomAccessFile(file, "rw"); fileChannel = fis.getChannel(); fl = fileChannel.tryLock(); if(fl == null || !fl.isValid()){ return false; } log.info("threadId = {} lock success", Thread.currentThread()); // if append if(append){ long length = fis.length(); fis.seek(length); fis.writeUTF(strToWrite); //if not, clear the content , then write }else{ fis.setLength(0); fis.writeUTF(strToWrite); } long tsEnd = System.currentTimeMillis(); long totalCost = (tsEnd - tsBegin); if(totalCost < lockTime){ Thread.sleep(lockTime - totalCost); } } catch (Exception e) { log.error("RandomAccessFile error",e); return false; }finally{ if(fl != null){ try { fl.release(); } catch (IOException e) { e.printStackTrace(); } } if(fileChannel != null){ try { fileChannel.close(); } catch (IOException e) { e.printStackTrace(); } } if(fis != null){ try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } return true; }
一切看起來都是那么美好,似乎無懈可擊。于是加上兩種測試場景代碼:
(1)同一進(jìn)程,兩個線程同時爭奪鎖,暫定命名為測試程序A,期待結(jié)果:有一線程獲取鎖失敗
(2)執(zhí)行兩個進(jìn)程,也就是執(zhí)行兩個測試程序A,期待結(jié)果:有一進(jìn)程某線程獲得鎖,另一線程獲取鎖失敗
public static void main(String[] args) { new Thread("write-thread-1-lock"){ @Override public void run() { FileLockUtils.lockAndWrite(new File("/data/hello.txt"), "write-thread-1-lock" + System.currentTimeMillis(), false, 30 * 1000);} }.start(); new Thread("write-thread-2-lock"){ @Override public void run() { FileLockUtils.lockAndWrite(new File("/data/hello.txt"), "write-thread-2-lock" + System.currentTimeMillis(), false, 30 * 1000); } }.start(); }
2.世界不像你想的那樣
上面的測試代碼在單個進(jìn)程內(nèi)可以達(dá)到我們的期待。但是同時運(yùn)行兩個進(jìn)程,在Mac環(huán)境(java8) 第二個進(jìn)程也能正常獲取到鎖,在Win7(java7)第二個進(jìn)程則不能獲取到鎖。為什么?難道TryLock不是排他的?
其實(shí)不是TryLock不是排他,而是channel.close 的問題,官方說法:
On some systems, closing a channel releases all locks held by the Java virtual machine on the underlying file regardless of whether the locks were acquired via that channel or via another channel open on the same file.It is strongly recommended that, within a program, a unique channel be used to acquire all locks on any given file.
原因就是在某些操作系統(tǒng),close某個channel將會導(dǎo)致JVM釋放所有l(wèi)ock。也就是說明了上面的第二個測試用例為什么會失敗,因?yàn)榈谝粋€進(jìn)程的第二個線程獲取鎖失敗后,我們調(diào)用了channel.close ,所有將會導(dǎo)致釋放所有l(wèi)ock,所有第二個進(jìn)程將成功獲取到lock。
在經(jīng)過一段曲折尋找真理的道路后,終于在stackoverflow上找到一個帖子 ,指明了 lucence 的 NativeFSLock,NativeFSLock 也是存在多個進(jìn)程排他寫的需求。筆者參考的是lucence 4.10.4 的NativeFSLock源碼,具體可見地址,具體可見obtain 方法,NativeFSLock 的設(shè)計思想如下:
(1)每一個鎖,都有本地對應(yīng)的文件。
(2)本地一個static類型線程安全的Set<String> LOCK_HELD維護(hù)目前所有鎖的文件路徑,避免多線程同時獲取鎖,多線程獲取鎖只需判斷LOCK_HELD是否已有對應(yīng)的文件路徑,有則表示鎖已被獲取,否則則表示沒被獲取。
(3)假設(shè)LOCK_HELD 沒有對應(yīng)文件路徑,則可對File的channel TryLock。
public synchronized boolean obtain() throws IOException { if (lock != null) { // Our instance is already locked: return false; } // Ensure that lockDir exists and is a directory. if (!lockDir.exists()) { if (!lockDir.mkdirs()) throw new IOException("Cannot create directory: " + lockDir.getAbsolutePath()); } else if (!lockDir.isDirectory()) { // TODO: NoSuchDirectoryException instead? throw new IOException("Found regular file where directory expected: " + lockDir.getAbsolutePath()); } final String canonicalPath = path.getCanonicalPath(); // Make sure nobody else in-process has this lock held // already, and, mark it held if not: // This is a pretty crazy workaround for some documented // but yet awkward JVM behavior: // // On some systems, closing a channel releases all locks held by the // Java virtual machine on the underlying file // regardless of whether the locks were acquired via that channel or via // another channel open on the same file. // It is strongly recommended that, within a program, a unique channel // be used to acquire all locks on any given // file. // // This essentially means if we close "A" channel for a given file all // locks might be released... the odd part // is that we can't re-obtain the lock in the same JVM but from a // different process if that happens. Nevertheless // this is super trappy. See LUCENE-5738 boolean obtained = false; if (LOCK_HELD.add(canonicalPath)) { try { channel = FileChannel.open(path.toPath(), StandardOpenOption.CREATE, StandardOpenOption.WRITE); try { lock = channel.tryLock(); obtained = lock != null; } catch (IOException | OverlappingFileLockException e) { // At least on OS X, we will sometimes get an // intermittent "Permission Denied" IOException, // which seems to simply mean "you failed to get // the lock". But other IOExceptions could be // "permanent" (eg, locking is not supported via // the filesystem). So, we record the failure // reason here; the timeout obtain (usually the // one calling us) will use this as "root cause" // if it fails to get the lock. failureReason = e; } } finally { if (obtained == false) { // not successful - clear up and move // out clearLockHeld(path); final FileChannel toClose = channel; channel = null; closeWhileHandlingException(toClose); } } } return obtained; }
總結(jié)
以上就是本文關(guān)于Java編程實(shí)現(xiàn)排他鎖代碼詳解的全部內(nèi)容,感興趣的朋友可以參閱:Java并發(fā)編程之重入鎖與讀寫鎖、詳解java中的互斥鎖信號量和多線程等待機(jī)制、Java語言中cas指令的無鎖編程實(shí)現(xiàn)實(shí)例以及本站其他相關(guān)專題,希望對大家有所幫助。如有不足之處,歡迎留言指出,小編一定及時更正,給大家提供更好的閱讀環(huán)境和幫助,感謝朋友們對本站的支持
相關(guān)文章
Dapr在Java中的服務(wù)調(diào)用實(shí)戰(zhàn)過程詳解
這篇文章主要為大家介紹了Dapr在Java中的服務(wù)調(diào)用實(shí)戰(zhàn)過程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06springboot之redis cache TTL選項(xiàng)的使用
這篇文章主要介紹了springboot之redis cache TTL選項(xiàng)的使用方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07mybatis 查詢sql中in條件用法詳解(foreach)
這篇文章主要介紹了mybatis 查詢sql中in條件用法詳解(foreach),具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(56)
下面小編就為大家?guī)硪黄狫ava基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧,希望可以幫到你2021-08-08java多線程Synchronized實(shí)現(xiàn)可見性原理解析
這篇文章主要介紹了java多線程Synchronized實(shí)現(xiàn)可見性原理,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-12-12intellij idea隱藏.iml和.idea等自動生成文件的問題
這篇文章主要介紹了intellij idea隱藏.iml和.idea等自動生成文件的問題,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09Spring占位符Placeholder的實(shí)現(xiàn)原理解析
這篇文章主要介紹了Spring占位符Placeholder的實(shí)現(xiàn)原理,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03