NIO - File Locking

        在 JDK 1.4 版本之前,Java I/O 模型都未能提供文件锁定(file locking),缺少这一特性让人们很头疼。绝大多数现代操作系统早就有了文件锁定功能,而直到 JDK 1.4 版本发布时 Java 编程人员才可以使用文件锁(file lock)。

        锁(lock)可以是共享的(shared)或独占的(exclusive)。这里描述的文件锁定特性在很大程度上依赖本地的操作系统实现。并非所有的操作系统和文件系统都支持共享文件锁。对于那些不支持的,对一个共享锁的请求会被自动提升为对独占锁的请求。这可以保证准确性却可能严重影响性能。

        并非所有平台都以同一个方式来实现基本的文件锁定。在不同的操作系统上,甚至在同一个操作系统的不同文件系统上,文件锁定的语义都会有所差异。一些操作系统仅提供劝告锁定(advisory locking),一些仅提供独占锁(exclusive locks),而有些操作系统可能两种锁都提供。你应该总是按照劝告锁的假定来管理文件锁,因为这是最安全的。但是如能了解底层操作系统如何执行锁定也是非常好的。例如,如果所有的锁都是强制性的(mandatory)而您不及时释放您获得的锁的话,运行在同一操作系统上的其他程序可能会受到影响。

        锁与文件关联,而不是与通道关联。我们使用锁来判优外部进程(决定哪个进程优先访问文件),而不是判优同一个 Java 虚拟机上的线程。如果一个线程在某个文件上获得了一个独占锁,然后第二个线程利用一个单独打开的通道来请求该文件的独占锁,那么第二个线程的请求会被批准。但如果这两个线程运行在不同的 Java 虚拟机上,那么第二个线程会阻塞,因为锁最终是由操作系统或文件系统来判优的并且几乎总是在进程级而非线程级上判优。

        有关 FileChannel 实现的文件锁定模型的一个重要注意项是:锁的对象是文件而不是通道或线程。 与文件锁定有关的 FileChannel API 方法:
public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel {

    // This is a partial API listing

    // 等价于 lock(0L, Long.MAX_VALUE, false)
    public final FileLock lock();
    /**
     * An invocation of this method will block until the region can be locked
     *
     * 锁是在文件内部区域上获得的。
     * 
     * 指定文件内部锁定区域开始于 position 并锁定 size 大小的区域。
     * shared 表示你想获取的锁是共享的(参数值为 true)还是独占的(参数值为 false)。
     * 要获得一个共享锁,你必须先以只读权限打开文件,而请求独占锁时则需要写权限。
     * 另外,提供的 position 和 size 参数的值不能是负数。
     *
     * 锁定区域的范围不一定要限制在文件的 size 值以内,锁可以扩展从而超出文件尾。因此,我们可以提前把
     * 待写入数据的区域锁定,我们也可以锁定一个不包含任何文件内容的区域,比如文件最后一个字节以外的区
     * 域。如果之后文件增长到达那块区域,那么您的文件锁就可以保护该区域的文件内容了。相反地,如果你
     * 锁定了文件的某一块区域,然后文件增长超出了那块区域,那么新增加的文件内容将不会受到您的文件锁
     * 的保护。
     */
    public abstract FileLock lock(long position, long size, boolean shared);
    public final FileLock tryLock();
    // lock() 方法的非阻塞变体
    public abstract FileLock tryLock(long position, long size, boolean shared);
}

        Java Doc 上这样描述 FileChannel 的 lock 方法:An invocation of this method will block until the region can be locked. lock 方法会阻塞?和上面描述的貌似不一样?上面描述:如果在同一虚拟机中,另外一个线程调用 lock 会批准,不矛盾吗?其实不然,Java Doc 上描述的内容指的是进程间而不是线程间。再次证明,锁的对象是文件而不是通道或线程。

        在上面的 API 列表中有两个名为 tryLock() 的方法,它们是 lock() 方法的非阻塞变体。这两个 tryLock() 和 lock() 方法起相同的作用,不过如果请求的锁不能立即获取到则会返回一个 null。

        lock() 和 tryLock() 方法均返回一个 FileLock 对象。 完整的 FileLock API:
public abstract class FileLock {
    public final FileChannel channel();
    public final long position();
    public final long size();
    public final boolean isShared();
    public final boolean overlaps(long position, long size);
    public abstract boolean isValid();
    public abstract void release() throws IOException;
}

        下面简述部分方法的作用:
  • channel():FileLock 对象由 FileChannel 创建并且总是关联到那个特定的通道实例。可以通过调用 channel() 方法来查询一个 lock 对象以判断它是由哪个通道创建的。
  • isValid():一个 FileLock 对象创建之后即有效,直到它的 release() 方法被调用或它所关联的通道被关闭或 Java 虚拟机关闭时才会失效。我们可以通过调用 isValid() 布尔方法来测试一个锁的有效性。一个锁的有效性可能会随着时间而改变,不过它的其他属性——位置(position)、范围大小(size)和独占性(exclusivity)——在创建时即被确定,不会随着时间而改变。
  • isShared():isShared() 方法用来测试一个锁以判断它是共享的还是独占的。如果底层的操作系统或文件系统不支持共享锁,那么该方法将总是返回 false 值,即使你申请锁时传递的参数值是 true。FileLock 对象是线程安全的,多个线程可以并发访问一个锁对象。
  • overlaps():查询一个 FileLock 对象是否与一个指定的文件区域重叠。这将你您可以迅速判断你拥有的锁是否与一个感兴趣的区域(region of interest)有交叉。不过即使返回值是 false 也不能保证你就一定能在期望的区域上获得一个锁,因为 Java 虚拟机上的其他地方或者外部进程可能已经在该期望区域上有一个或多个锁了。你最好使用 tryLock()方法确认一下。
        尽管一个 FileLock 对象是与某个特定的 FileChannel 实例关联的,它所代表的锁却是与一个底层文件关联的,而不是与通道关联。因此,如果你在使用完一个锁后而不释放它的话,可能会导致冲突或者死锁。在发生错误时,请务必使用 finally 来释放你获得的锁。

        有关独占锁和共享锁,这里总结下:
  1. 独占锁和共享锁都是对文件的部分区域或全部区域进行锁定,然后对这些被锁定的区域做一些操作。
  2. 独占锁被某进程获得后,其它进程若想再获得独占锁,必须等上一个进程释放锁后才能获得。
  3. 文件的某部分区域被一进程获得的独占锁锁定,那么另外一个进程即便是申请同样区域的共享锁,也必须等该块区域的独占锁的进程释放锁才能得到共享锁。
  4. 共享锁被某进程获得后,其它进程是可以再次得到共享锁,并对共享锁锁定的区域进行操作。
  5. 文件的某部分区域被一进程获得的共享锁锁定,那么另外一个进程申请同样区域的独占锁锁,也必须等得到该块区域的共享锁锁的所有进程(这里用的是所有,因为共享锁一旦被某进程获得,其它进程也毫不费吹灰之力也可以得到共享锁)释放锁才能得到独占锁。当然,进程不想等的话,可以得到共享锁。
  6. 上面描述进程得到锁后,用的是可以做一些操作,并没指定要做什么操作。你可以用独占锁来做读操作,用共享锁来做写操作,当然没问题。只要能满足你的需求,你想怎么做都行。

你可能感兴趣的:(File)