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:
(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
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.
(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'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!