Java编程实现排他锁代码详解

 更新时间:2017年10月16日 14:33:58   作者:jacin1  
这篇文章主要介绍了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类型线程安全的Set<String> 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'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编程实现排他锁代码详解的全部内容,感兴趣的朋友可以参阅:Java并发编程之重入锁与读写锁详解java中的互斥锁信号量和多线程等待机制Java语言中cas指令的无锁编程实现实例以及本站其他相关专题,希望对大家有所帮助。如有不足之处,欢迎留言指出,小编一定及时更正,给大家提供更好的阅读环境和帮助,感谢朋友们对本站的支持

相关文章

  • Java五种方式实现多线程循环打印问题

    Java五种方式实现多线程循环打印问题

    本文主要介绍了Java五种方式实现多线程循环打印问题,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • Spring框架之IOC介绍讲解

    Spring框架之IOC介绍讲解

    IOC-Inversion of Control,即控制反转。它不是什么技术,而是一种设计思想。这篇文章将为大家介绍一下Spring控制反转IOC的原理,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-08-08
  • Java String类和StringBuffer类的区别介绍

    Java String类和StringBuffer类的区别介绍

    这篇文章主要介绍了Java String类和StringBuffer类的区别, 关于java的字符串处理我们一般使用String类和StringBuffer类有什么不同呢,下面我们一起来看看详细介绍吧
    2022-03-03
  • 一篇文章带你入门java泛型

    一篇文章带你入门java泛型

    这篇文章主要介绍了java泛型基础知识及通用方法,从以下几个方面介绍一下java的泛型: 基础, 泛型关键字, 泛型方法, 泛型类和接口,感兴趣的可以了解一下
    2021-08-08
  • mybatis3.3+struts2.3.24+mysql5.1.22开发环境搭建图文教程

    mybatis3.3+struts2.3.24+mysql5.1.22开发环境搭建图文教程

    这篇文章主要为大家详细介绍了mybatis3.3+struts2.3.24+mysql5.1.22开发环境搭建图文教程,感兴趣的小伙伴们可以参考一下
    2016-06-06
  • Intellij Idea 多模块Maven工程中模块之间无法相互引用问题

    Intellij Idea 多模块Maven工程中模块之间无法相互引用问题

    这篇文章主要介绍了Intellij Idea 多模块Maven工程中模块之间无法相互引用问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • Java8中如何通过方法引用获取属性名详解

    Java8中如何通过方法引用获取属性名详解

    这篇文章主要给大家介绍了关于Java8中如何通过方法引用获取属性名的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • Java面试之如何实现10亿数据判重

    Java面试之如何实现10亿数据判重

    当数据量比较大时,使用常规的方式来判重就不行了,所以这篇文章小编主要来和大家介绍一下Java实现10亿数据判重的相关方法,希望对大家有所帮助
    2024-02-02
  • Java基于Socket实现网络编程实例详解

    Java基于Socket实现网络编程实例详解

    本文主要给大家介绍的是Java基于Socket实现网络编程的实例,并给大家介绍了TCP与UDP传输协议,有需要的小伙伴可以来参考下
    2016-07-07
  • 数据同步利器DataX简介及如何使用

    数据同步利器DataX简介及如何使用

    DataX 是阿里云 DataWorks数据集成 的开源版本,使用Java 语言编写,在阿里巴巴集团内被广泛使用的离线数据同步工具/平台,今天给大家分享一个阿里开源的数据同步工具DataX,在Github拥有14.8k的star,非常受欢迎
    2024-02-02

最新评论