Java编程实现排他锁代码详解
一.前言
某年某月某天,同事说需要一个文件排他锁功能,需求如下:
(1)写操作是排他属性
(2)适用于同一进程的多线程/也适用于多进程的排他操作
(3)容错性:获得锁的进程若Crash,不影响到后续进程的正常获取锁
二.解决方案
1.最初的构想
在Java领域,同进程的多线程排他实现还是较简易的。比如使用线程同步变量标示是否已锁状态便可。但不同进程的排他实现就比较繁琐。使用已有API,自然想到java.nio.channels.FileLock:如下
/** *@paramfile *@paramstrToWrite *@paramappend *@paramlockTime以毫秒为单位,该值只是方便模拟排他锁时使用,-1表示不考虑该字段 *@return */ publicstaticbooleanlockAndWrite(Filefile,StringstrToWrite,booleanappend,intlockTime){ if(!file.exists()){ returnfalse; } RandomAccessFilefis=null; FileChannelfileChannel=null; FileLockfl=null; longtsBegin=System.currentTimeMillis(); try{ fis=newRandomAccessFile(file,"rw"); fileChannel=fis.getChannel(); fl=fileChannel.tryLock(); if(fl==null||!fl.isValid()){ returnfalse; } log.info("threadId={}locksuccess",Thread.currentThread()); //ifappend if(append){ longlength=fis.length(); fis.seek(length); fis.writeUTF(strToWrite); //ifnot,clearthecontent,thenwrite }else{ fis.setLength(0); fis.writeUTF(strToWrite); } longtsEnd=System.currentTimeMillis(); longtotalCost=(tsEnd-tsBegin); if(totalCost一切看起来都是那么美好,似乎无懈可击。于是加上两种测试场景代码:
(1)同一进程,两个线程同时争夺锁,暂定命名为测试程序A,期待结果:有一线程获取锁失败
(2)执行两个进程,也就是执行两个测试程序A,期待结果:有一进程某线程获得锁,另一线程获取锁失败publicstaticvoidmain(String[]args){ newThread("write-thread-1-lock"){ @Override publicvoidrun(){ FileLockUtils.lockAndWrite(newFile("/data/hello.txt"),"write-thread-1-lock"+System.currentTimeMillis(),false,30*1000);} }.start(); newThread("write-thread-2-lock"){ @Override publicvoidrun(){ FileLockUtils.lockAndWrite(newFile("/data/hello.txt"),"write-thread-2-lock"+System.currentTimeMillis(),false,30*1000); } }.start(); }2.世界不像你想的那样
上面的测试代码在单个进程内可以达到我们的期待。但是同时运行两个进程,在Mac环境(java8)第二个进程也能正常获取到锁,在Win7(java7)第二个进程则不能获取到锁。为什么?难道TryLock不是排他的?
其实不是TryLock不是排他,而是channel.close的问题,官方说法:
Onsomesystems,closingachannelreleasesalllocksheldbytheJavavirtualmachineonthe underlyingfileregardlessofwhetherthelockswereacquiredviathatchannelorvia anotherchannelopenonthesamefile.Itisstronglyrecommendedthat,withinaprogram,aunique channelbeusedtoacquirealllocksonanygivenfile.原因就是在某些操作系统,close某个channel将会导致JVM释放所有lock。也就是说明了上面的第二个测试用例为什么会失败,因为第一个进程的第二个线程获取锁失败后,我们调用了channel.close,所有将会导致释放所有lock,所有第二个进程将成功获取到lock。
在经过一段曲折寻找真理的道路后,终于在stackoverflow上找到一个帖子,指明了lucence的NativeFSLock,NativeFSLock也是存在多个进程排他写的需求。笔者参考的是lucence4.10.4的NativeFSLock源码,具体可见地址,具体可见obtain方法,NativeFSLock的设计思想如下:
(1)每一个锁,都有本地对应的文件。
(2)本地一个static类型线程安全的SetLOCK_HELD维护目前所有锁的文件路径,避免多线程同时获取锁,多线程获取锁只需判断LOCK_HELD是否已有对应的文件路径,有则表示锁已被获取,否则则表示没被获取。
(3)假设LOCK_HELD没有对应文件路径,则可对File的channelTryLock。publicsynchronizedbooleanobtain()throwsIOException{ if(lock!=null){ //Ourinstanceisalreadylocked: returnfalse; } //EnsurethatlockDirexistsandisadirectory. if(!lockDir.exists()){ if(!lockDir.mkdirs()) thrownewIOException("Cannotcreatedirectory:"+lockDir.getAbsolutePath()); }elseif(!lockDir.isDirectory()){ //TODO:NoSuchDirectoryExceptioninstead? thrownewIOException("Foundregularfilewheredirectoryexpected:"+lockDir.getAbsolutePath()); } finalStringcanonicalPath=path.getCanonicalPath(); //Makesurenobodyelsein-processhasthislockheld //already,and,markitheldifnot: //Thisisaprettycrazyworkaroundforsomedocumented //butyetawkwardJVMbehavior: // //Onsomesystems,closingachannelreleasesalllocksheldbythe //Javavirtualmachineontheunderlyingfile //regardlessofwhetherthelockswereacquiredviathatchannelorvia //anotherchannelopenonthesamefile. //Itisstronglyrecommendedthat,withinaprogram,auniquechannel //beusedtoacquirealllocksonanygiven //file. // //Thisessentiallymeansifweclose"A"channelforagivenfileall //locksmightbereleased...theoddpart //isthatwecan'tre-obtainthelockinthesameJVMbutfroma //differentprocessifthathappens.Nevertheless //thisissupertrappy.SeeLUCENE-5738 booleanobtained=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|OverlappingFileLockExceptione){ //AtleastonOSX,wewillsometimesgetan //intermittent"PermissionDenied"IOException, //whichseemstosimplymean"youfailedtoget //thelock".ButotherIOExceptionscouldbe //"permanent"(eg,lockingisnotsupportedvia //thefilesystem).So,werecordthefailure //reasonhere;thetimeoutobtain(usuallythe //onecallingus)willusethisas"rootcause" //ifitfailstogetthelock. failureReason=e; } }finally{ if(obtained==false){//notsuccessful-clearupandmove //out clearLockHeld(path); finalFileChanneltoClose=channel; channel=null; closeWhileHandlingException(toClose); } } } returnobtained; }总结
以上就是本文关于Java编程实现排他锁代码详解的全部内容,感兴趣的朋友可以参阅:Java并发编程之重入锁与读写锁、详解java中的互斥锁信号量和多线程等待机制、Java语言中cas指令的无锁编程实现实例以及本站其他相关专题,希望对大家有所帮助。如有不足之处,欢迎留言指出,小编一定及时更正,给大家提供更好的阅读环境和帮助,感谢朋友们对本站的支持