java I/O系统(7)-文件加锁

引言

在通道中我们可以对文件或者部分文件进行上锁。上锁和我们了解的线程锁差不多,都是为了保证数据的一致性。在文件通道FileChannel中可以对文件进行上锁,通过FileLock可以对文件进行锁的释放。在本篇博文中会详细介绍在NIO中对文件锁的操作与配置,同时给出对应demo。笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290

文件通道FileChannel

文件加锁是建立在文件通道(FileChannel)之上的,套接字通道(SockeChannel)不考虑文件加锁,因为它是不共享的。它对文件加锁有两种方式:
①lock


   public final FileLock lock() throws IOException {
        return lock(0L, Long.MAX_VALUE, false);
    }

   public abstract FileLock lock(long position, long size, boolean shared)
        throws IOException;

②tryLock

 public final FileLock tryLock() throws IOException {
        return tryLock(0L, Long.MAX_VALUE, false);
    }
 public abstract FileLock tryLock(long position, long size, boolean shared)
        throws IOException;

两种加锁方式默认都是对整个文件加锁,如果自己配置的话就可以控制加锁的文件范围:position是加锁的开始位置,size是加锁长度,shared是用于控制该锁是共享的还是独占的。
那么两个到底有什么区别呢?
lock是阻塞式的,当有进程对锁进行读取时会等待锁的释放,在此期间它会一直等待;tryLock是非阻塞式的,它尝试获得锁,如果这个锁不能获得,那么它会立即返回。
其实,这个锁的机制有点像多线程中的Lock锁,可以参考以前博文,其中有对synchronized与lock的比较(http://blog.csdn.net/u012403290/article/details/64910926

下面这段代码区别了这个其中的关系:

package com.brickworkers.io.nio;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.nio.channels.FileLock;

/** * * @author Brickworker * Date:2017年5月22日下午3:06:55 * 关于类LockingTest.java的描述:NIO文件加锁 * Copyright (c) 2017, brcikworker All Rights Reserved. */
public class LockingTest {

    private static class MyThread extends Thread{
        private FileOutputStream fs;
        private boolean isTrylock;

        public MyThread(FileOutputStream fs, boolean isTrylock) {
            this.fs = fs;
            this.isTrylock = isTrylock;
        }
        @Override
        public void run() {
            try {
                //获取锁
                FileLock fl;
                if(isTrylock){
                    fl = fs.getChannel().tryLock();
                    System.out.println(this.getName() + "获取tryLock非阻塞锁");
                }else{
                    fl = fs.getChannel().lock();
                    System.out.println(this.getName() + "获取lock阻塞锁");
                }

                //等待5秒释放锁
                Thread.sleep(10000);
                fl.release();
                System.out.println(this.getName() + "释放锁");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws FileNotFoundException{
        FileOutputStream fs = new FileOutputStream("F:/java/io/in.txt");
        //实现2个线程去对文件进行不同方式的加锁
        MyThread t1 = new MyThread(fs, true);
        MyThread t2 = new MyThread(fs, true);
        t1.start();
        t2.start();

    }

}

但是,如果你对这段代码进行测试,发现是报错的,java.nio.channels.OverlappingFileLockException。为什么会这样呢?因为在一个进程中在锁对象没有释放的过程第二次尝试进行加锁就会抛出这个错误,我们所说的阻塞,经过博主的测试时建立在两个进程之间,我们可以创建进程(Process)但是显得有些繁琐,我们可以用一个类来启动锁的占用,用宁外一个类进行测试:
以下是一个前提类,主要用于对锁进行获取:

package com.brickworkers.io.nio;

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileLock;

/** * * @author Brickworker * Date:2017年5月22日下午3:06:55 * 关于类LockingTest.java的描述:NIO文件加锁 * Copyright (c) 2017, brcikworker All Rights Reserved. */
public class LockingTest {

    public static void main(String[] args) throws IOException, InterruptedException{
        try(FileOutputStream fs = new FileOutputStream("F:/java/io/in.txt");){
            //直接获取文件所
            //在这里我们不考虑锁类型,只要把所占用起来就可以
            FileLock fl = fs.getChannel().lock();
            //线程等待10秒
            Thread.sleep(10000);
            //释放锁
            fl.release();
        }
    }

}

这个类表示一个进程,用于首先获得锁,并把锁控制10秒钟。在运行上面这个类之后,立马运行测试类,测试类包括可以对lock与tryLock进行比较:

package com.brickworkers.io.nio;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileLock;

public class LockingFoceTest {

    // 不同的锁获取方式
    private static void lock(boolean isTrylock) throws FileNotFoundException, IOException {
        try (FileOutputStream fs = new FileOutputStream("F:/java/io/in.txt")) {
            long startTime = System.currentTimeMillis();
            if (isTrylock) {
                FileLock fl = fs.getChannel().tryLock();
                //tryLock在锁被占用的时候会返回null
                while(fl == null){
                    fl = fs.getChannel().tryLock();
                }
                System.out.println("非阻塞式经过" + (System.currentTimeMillis() - startTime)/1000 + "秒,终于获得该锁");
                fl.release();
            } else {
                FileLock fl = fs.getChannel().lock();
                System.out.println("阻塞经过" + (System.currentTimeMillis() - startTime)/1000 + "秒,终于获得该锁");
                fl.release();
            }
        }
    }

    public static void main(String[] args) throws FileNotFoundException, IOException {
        lock(false);
// lock(true);
    }

}

大家可以根据要测试的所,合理注释main函数中的锁类型。注意,在运行LockingFoceTest 类之前一定要先运行LockingTest类,先要模拟一个进程占用了锁。这样讲,是不是更容易理解一些呢。
经过测试发现,lock方法是进程阻塞的,当它尝试获得一个文件的锁的时候,如果这个锁已经被占用,那么它会将进程挂起,等待对方把锁释放之后,再获取锁。而tryLock是非进程阻塞的,当它尝试获得一个文件锁的时候,如果这个锁被占用了,那么它会直接返回一个null值,而不会继续等待下去。

文件锁FileLock

在java的NIO中,通道包下面有一个FileLock类,它主要是对文件锁工具的一个描述。在上一小节中对文件的锁获取其实是FileChannel获取的(lock与trylock是FileChannel的方法),它们返回一个FileLock对象。这个类的核心方法有如下这些:
boolean isShared() :判断锁是否为共享类型
abstract boolean isValid() :判断锁是否有效
boolean overlaps():判断此锁定是否与给定的锁定区域重叠
long position():返回文件内锁定区域中第一个字节的位置。
abstract void release() :释放锁
long size() :返回锁定区域的大小,以字节为单位
我们会在下面一一解释方法的使用方式与意义。

文件锁的释放方式

在文件锁中有3种方式可以释放文件锁:①锁类释放锁,调用FileLock的release方法; ②通道类关闭通道,调用FileChannel的close方法;③jvm虚拟机会在特定情况释放锁。
下面是一个demo测试前面两种释放方式:

package com.brickworkers.io.nio;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

/** * * @author Brickworker * Date:2017年5月22日下午4:48:32 * 关于类LockReleaseTest.java的描述:文件锁的释放 * Copyright (c) 2017, brcikworker All Rights Reserved. */
public class LockReleaseTest {

    public static void main(String[] args) throws FileNotFoundException, IOException {
        try (FileOutputStream fs = new FileOutputStream("F:/java/io/in.txt")) {
            //获取通道
            FileChannel fc = fs.getChannel();
            //获取锁
            FileLock fl = fc.tryLock();
            System.out.println("获取锁");
            //调用FileLock的release方法
            fl.release();
            if(!fl.isValid()){
                System.out.println("release方法锁释放");
            }

            //重新获得锁
            //在一个进程中在锁没有释放之前是无法再次获得锁的
            fl = fc.tryLock();
            if(fl.isValid()){
                System.out.println("再次成功获得锁");
            }
            //关闭通道,释放锁
            fc.close();
            if(!fl.isValid()){
                System.out.println("通道释放锁");
            }
        }
    }

}
//执行结果:
//获取锁
//release方法锁释放
//再次成功获得锁
//通道释放锁
//
//

两种方式都是可以成功的释放文件锁。

锁类型(独占式和共享式)

我们先区分一下在文件锁中两种锁的区别:①独占式的锁就想我们上面测试的那样,只要有一个进程获取了独占锁,那么别的进程只能等待。②共享锁在一个进程获取的情况下,别的进程还是可以读取被锁定的文件,但是别的进程不能写只能读。
我们在前面说过,默认情况下是对整个文件进行上锁,查看构造函数其实只是设定了文件长度从0开始到最大为止。在前面的博文也说过,通道获取还可以从随机访问中来,所以下面我们用RandomAccessFile来进行测试,和上面一样,我们可以用一个类作为锁占有类:

package com.brickworkers.io.nio;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileLock;

/** * * @author Brickworker * Date:2017年5月22日下午3:06:55 * 关于类LockingTest.java的描述:NIO文件加锁 * Copyright (c) 2017, brcikworker All Rights Reserved. */
public class LockingTest {

    public static void main(String[] args) throws IOException, InterruptedException{
        try(RandomAccessFile rf = new RandomAccessFile("F:/java/io/in.txt", "rw")){
            //直接获取文件所
            //在这里我们不考虑锁类型,只要把所占用起来就可以
            FileLock fl = rf.getChannel().lock(0, 0x7fffffffffffffffL, true);
            if(fl.isShared()){
                System.out.println("设置共享锁成功");
            }else{
                System.out.println("设置独占锁成功");
            }
            //线程等待10秒
            Thread.sleep(20000);
            //释放锁
            fl.release();
        }
    }

}

这个类需要修改lock方法的第三个参数,从而设置你设置的这个锁的类型,如果是true就是共享锁,如果为false就是独占锁。在代码中0x7fffffffffffffffL的值是FileLock能够锁定文件大小的最大值。接着把这个类跑起来之后,尝试运行下面这个类:

package com.brickworkers.io.nio;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;

/** * * @author Brickworker * Date:2017年5月22日下午5:24:34 * 关于类LockTypeTest.java的描述:不同锁类型的获取结果 * Copyright (c) 2017, brcikworker All Rights Reserved. */
public class LockTypeTest {

    public static void main(String[] args) throws FileNotFoundException, IOException {
        try(RandomAccessFile rf = new RandomAccessFile("F:/java/io/in.txt", "rw")){
            //如果不是共享锁,那么不管是读还是写都是不可以的。但是如果是共享锁,那么读取时可以的,写却不行
            byte[] bytes1 = new byte[1024];
            rf.read(bytes1);
            System.out.println(new String(bytes1));



            //写数据
            byte[] write = "brickwork".getBytes();
            rf.write(write);
        }
    }

}

博主经过测试,如果前提类是独占锁,那么在这个测试中会提示“java.io.IOException: 另一个程序已锁定文件的一部分,进程无法访问。”如果是共享锁,那么文件读取是可以获得数据的,但是尝试写入的时候也会抛出这个错误。

锁定部分文件

在文件锁机制中,可以指定锁定的文件长度。其实在前面的代码中已经涉及了,就是lock构造的第一和第二个参数。那么锁定部分会造成锁冲突吗?下面是我们的测试:

package com.brickworkers.io.nio;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;

public class LockSubTest {

    public static void main(String[] args) throws FileNotFoundException, IOException, InterruptedException {
        try(RandomAccessFile rf = new RandomAccessFile("F:/java/io/in.txt", "rw")){
            //先写入一定的数据
            rf.write("my name is brickworker".getBytes());

            //获得通道
            FileChannel fc = rf.getChannel();

            //对前10个字节进行上锁
            FileLock flf = fc.lock(0, 10, false);
            if(flf.isValid()){
                System.out.println("对前10个字节上锁成功");
                System.out.println("锁定的范围从:" + flf.position() +" 至 "+ flf.size());
            }

            //对后半部分进行上锁
            FileLock fll = fc.lock(10, rf.length(), false);
            if(fll.isValid()){
                System.out.println("对后半部分上锁成功");
                System.out.println("锁定的范围从:" + fll.position() +" 至 "+ fll.size());
            }

            Thread.sleep(10000);
            //释放锁
            //直接关闭通道
            fc.close();
        }
    }

}


//运行结果:
//对前10个字节上锁成功
//锁定的范围从:0 至 10
//对后半部分上锁成功
//锁定的范围从:10 至 22
//
//

所以我们前面说的是不精准的,应该说:在一个进程中,我们对于文件的某一个点在没有释放锁只能上一次锁,且锁不能重叠。大家可以在最后的这段测试代码,设置后半段的上锁,把其实位置设置为小于9进行测试就可以发现所定区域是不能重叠的。

    原文作者:java锁
    原文地址: https://blog.csdn.net/u012403290/article/details/72627919
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞