首頁  >  文章  >  Java  >  Java排他鎖實現的程式碼詳解

Java排他鎖實現的程式碼詳解

黄舟
黄舟原創
2017-10-17 09:38:422025瀏覽

這篇文章主要介紹了Java程式實作排他鎖的相關內容,敘述了實現此程式碼鎖所需的功能,以及作者的解決方案,然後向大家分享了設計源碼,需要的朋友可以參考下。

一.前言

某年某月某天,同事說需要一個檔案排他鎖功能,需求如下:

(1)寫入操作是排他屬性
(2)適用於同一進程的多線程/也適用於多進程的排他操作
(3)容錯性:獲得鎖的進程若Crash,不影響到後續進程的正常取得鎖定

二.解決方案

##1. 最初的構想

在Java領域,同行程的多執行緒排他實作還是較簡易的。例如使用執行緒同步變數標示是否已鎖定狀態便可。但不同進程的排他實作就比較繁瑣。使用已有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)同一進程,兩個執行緒同時爭奪鎖,暫定命名為測試程式A,期待結果:有一執行緒取得鎖定失敗

(2)執行兩個行程,也就是執行兩個測試程式A,期待結果:有一進程某執行緒取得鎖,另一執行緒取得鎖定失敗

##

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.世界不像你想的那樣


上面的測試程式碼在單一進程內可以達到我們的期望。但同時運行兩個進程,在Mac環境(java8) 第二個進程也能正常取得到鎖,在Win7(java7)第二個進程則不能取得到鎖。為什麼?難道TryLock不是排他的?


其實不是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.

原因就是在某些作業系統, close某個channel將會導致JVM釋放所有lock。也就是說明了上面的第二個測試案例為什麼會失敗,因為第一個進程的第二個線程獲取鎖定失敗後,我們調用了channel.close ,所有將會導致釋放所有lock,所有第二個進程將成功獲取到lock。


在經過一段曲折尋找真理的道路後,終於在stackoverflow上找到一個帖子 ,指明了 lucence 的 NativeFSLock,NativeFSLock 也是存在多個進程排他寫的需求。筆者參考的是lucence 4.10.4 的NativeFSLock源碼,具體可見地址,具體可見obtain 方法,NativeFSLock 的設計思想如下:


(1)每一個鎖,都有本地對應的文件。

(2)本地一個static類型線程安全的Setf7e83be87db5cd2d9a8a0b8117b38cd4 LOCK_HELD維護目前所有鎖的檔案路徑,避免多執行緒同時取得鎖,多執行緒取得鎖定只需判斷LOCK_HELD是否已有對應的檔案路徑,有則表示鎖已被獲取,否則則表示沒被獲取。

(3)假設LOCK_HELD 沒有對應檔案路徑,則可對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&#39;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; 
  }

總結

以上是Java排他鎖實現的程式碼詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn