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指令的无锁编程实现实例以及本站其他相关专题,希望对大家有所帮助。如有不足之处,欢迎留言指出,小编一定及时更正,给大家提供更好的阅读环境和帮助,感谢朋友们对本站的支持