Home  >  Article  >  Java  >  Detailed code explanation of Java exclusive lock implementation

Detailed code explanation of Java exclusive lock implementation

黄舟
黄舟Original
2017-10-17 09:38:422024browse

This article mainly introduces the relevant content of Java programming to implement exclusive lock, describes the functions required to implement this code lock, and the author's solution, and then shares the design source code with everyone. Friends in need can refer to it.

1. Foreword

On a certain day of a certain year, a certain month, a colleague said that a file exclusive lock function is needed. The requirements are as follows:

(1) The write operation is an exclusive property
(2) Applicable to multi-threads of the same process/also applicable to exclusive operations of multiple processes
(3) Fault tolerance: acquiring the lock If the process crashes, it will not affect the normal acquisition of locks by subsequent processes

2. Solution

1. Initially Concept

In the Java field, the exclusive implementation of multi-threading in the same process is relatively simple. For example, you can use a thread synchronization variable to indicate whether it is locked. But the exclusive implementation of different processes is more cumbersome. Using the existing API, I naturally think of java.nio.channels.FileLock: as follows


##

/** 
   * @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; 
  }

Everything looks so beautiful and seems to be impeccable. So two test scenario codes were added:


(1) In the same process, two threads compete for the lock at the same time. It is tentatively named test program A. The expected result: one thread failed to acquire the lock

(2) Execute two processes, that is, execute two test programs A, and expect the results: one process and a thread obtain the lock, and the other thread fails to obtain the lock


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. The world is not what you think

The above test code can meet our expectations in a single process. However, when running two processes at the same time, the second process can obtain the lock normally in the Mac environment (java8), but the second process cannot obtain the lock in Win7 (java7). Why? Isn't TryLock exclusive?


In fact, it’s not that TryLock is not exclusive, but a problem with channel.close. The official statement is:


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.

The reason is that in some operating systems, Closing a channel will cause the JVM to release all locks. In other words, we understand why the second test case above failed, because after the second thread of the first process failed to acquire the lock, we called channel.close, which will cause all locks to be released, and all second processes The lock will be obtained successfully.


After a tortuous journey to find the truth, I finally found a post on stackoverflow, which pointed out that Lucence’s NativeFSLock also requires exclusive writing by multiple processes. The author refers to the NativeFSLock source code of lucence 4.10.4. The specific address can be seen, and the obtain method can be seen specifically. The design ideas of NativeFSLock are as follows:


(1) Each lock has a local corresponding file.

(2) A local static type thread-safe Setf7e83be87db5cd2d9a8a0b8117b38cd4 LOCK_HELD maintains the file paths of all current locks to avoid multiple threads acquiring locks at the same time. To acquire locks, multiple threads only need to determine whether LOCK_HELD already has a corresponding file path. It means that the lock has been acquired, otherwise it means that it has not been acquired.
(3) Assuming that LOCK_HELD does not have a corresponding file path, you can TryLock the File channel.


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; 
  }

Summary

The above is the detailed content of Detailed code explanation of Java exclusive lock implementation. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn