内容目录
3.3.2 文件锁定
在 JDK 1.4 版本之前,Java I/O 模型都未能提供文件锁定(file locking),缺少这一特性让人们很头疼。绝大多数现代操作系统早就有了文件锁定功能,而直到 JDK 1.4 版本发布时 Java 编程人员才可以使用文件锁(file lock)。在集成许多其他非Java程序时,文件锁定显得尤其重要。此外,它在判优(判断多个访问请求的优先级别)一个大系统的多个Java组件发起的访问时也很有价值。
我们在第一章中讨论到,锁(lock)可以是共享的(shared)或独占的(exclusive)。本节中描述的文件锁定特性在很大程度上依赖本地的操作系统实现。并非所有的操作系统和文件系统都支持共享文件锁。对于那些不支持的,对一个共享锁的请求会被自动提升为对独占锁的请求。这可以保证准确性却可能严重影响性能。举个例子,仅使用独占锁将会串行化图 1-7 中所列的全部reader进程。如果您计划部署程序,请确保您了解所用操作系统和文件系统的文件锁定行为,因为这将严重影响您的设计选择。
另外,并非所有平台都以同一个方式来实现基本的文件锁定。在不同的操作系统上,甚至在同一个操作系统的不同文件系统上,文件锁定的语义都会有所差异。一些操作系统仅提供劝告锁定(advisory locking),一些仅提供独占锁(exclusive locks),而有些操作系统可能两种锁都提供。
您应该总是按照劝告锁的假定来管理文件锁,因为这是最安全的。但是如能了解底层操作系统如何执行锁定也是非常好的。例如,如果所有的锁都是强制性的(mandatory)而您不及时释放您获得的锁的话,运行在同一操作系统上的其他程序可能会受到影响。
有关 FileChannel 实现的文件锁定模型的一个重要注意项是:锁的对象是文件而不是通道或线程,这意味着文件锁不适用于判优同一台 Java 虚拟机上的多个线程发起的访问。如果一个线程在某个文件上获得了一个独占锁,然后第二个线程利用一个单独打开的通道来请求该文件的独占锁,那么第二个线程的请求会被批准。但如果这两个线程运行在不同的 Java 虚拟机上,那么第二个线程会阻塞,因为锁最终是由操作系统或文件系统来判优的并且几乎总是在进程级而非线程级上判优。锁都是与一个文件关联的,而不是与单个的文件句柄或通道关联。
锁与文件关联,而不是与通道关联。我们使用锁来判优外部进程,而不是判优同一个Java虚拟机上的线程。
文件锁旨在在进程级别上判优文件访问,比如在主要的程序组件之间或者在集成其他供应商的组件时。如果您需要控制多个 Java 线程的并发访问,您可能需要实施您自己的、轻量级的锁定方案。那种情形下,内存映射文件(本章后面会进行详述)可能是一个合适的选择。
现在让我们来看下与文件锁定有关的FileChannel API方法:
public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel { // 这里仅列出部分API public final FileLock lock() public abstract FileLock lock (long position, long size, boolean shared) public final FileLock tryLock() public abstract FileLock tryLock (long position, long size, boolean shared) }
这次我们先看带参数形式的lock()
方法。锁是在文件内部区域上获得的。调用带参数的lock()
方法会指定文件内部锁定区域的开始position
以及锁定区域的size
。第三个参数shared
表示您想获取的锁是共享的(参数值为true
)还是独占的(参数值为false
)。要获得一个共享锁,您必须先以只读权限打开文件,而请求独占锁时则需要写权限。另外,您提供的position
和size
参数的值不能是负数。
锁定区域的范围不一定要限制在文件的size
值以内,锁可以扩展从而超出文件尾。因此,我们可以提前把待写入数据的区域锁定,我们也可以锁定一个不包含任何文件内容的区域,比如文件最后一个字节以外的区域。如果之后文件增长到达那块区域,那么您的文件锁就可以保护该区域的文件内容了。相反地,如果您锁定了文件的某一块区域,然后文件增长超出了那块区域,那么新增加 的文件内容将不会受到您的文件锁的保护。
不带参数的简单形式的lock()
方法是一种在整个文件上请求独占锁的便捷方法,锁定区域等于它能达到的最大范围。该方法等价于:
fileChannel.lock(0L, Long.MAX_VALUE, false);
如果您正请求的锁定范围是有效的,那么lock()
方法会阻塞,它必须等待前面的锁被释放。假如您的线程在此情形下被暂停,该线程的行为受中断语义(类似我们在 3.1.3 节中所讨论的)控制。如果通道被另外一个线程关闭,该暂停线程将恢复并产生一个 AsynchronousCloseException异常。假如该暂停线程被直接中断(通过调用它的interrupt()
方法),它将醒来并产生一个FileLockInterruptionException
异常。如果在调用lock()
方法时线程的interrupt status已经被设置,也会产生FileLockInterruptionException
异常。
在上面的 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; }
FileLock
类封装一个锁定的文件区域。FileLock
对象由FileChannel
创建并且总是关联到那个特定的通道实例。您可以通过调用channel()
方法来查询一个lock对象以判断它是由哪个通道创建的。
一个FileLock
对象创建之后即有效,直到它的release()
方法被调用或它所关联的通道被关闭或Java虚拟机关闭时才会失效。我们可以通过调用isValid()
布尔方法来测试一个锁的有效性。一个锁的有效性可能会随着时间而改变,不过它的其他属性——位置(position)、范围大小(size)和独占性(exclusivity)——在创建时即被确定,不会随着时间而改变。
您可以通过调用isShared()
方法来测试一个锁以判断它是共享的还是独占的。如果底层的操作系统或文件系统不支持共享锁,那么该方法将总是返回false
值,即使您申请锁时传递的参数值是true
。假如您的程序依赖共享锁定行为,请测试返回的锁以确保您得到了您申请的锁类型。FileLock
对象是线程安全的,多个线程可以并发访问一个锁对象。
最后,您可以通过调用overlaps()
方法来查询一个FileLock
对象是否与一个指定的文件区域重叠。这将使您可以迅速判断您拥有的锁是否与一个感兴趣的区域(region of interest)有交叉。不过即使返回值是false
也不能保证您就一定能在期望的区域上获得一个锁,因为Java虚拟机上的其他地方或者外部进程可能已经在该期望区域上有一个或多个锁了。您最好使用tryLock()
方法确认一下。
尽管一个FileLock
对象是与某个特定的FileChannel
实例关联的,它所代表的锁却是与一个底层文件关联的,而不是与通道关联。因此,如果您在使用完一个锁后而不释放它的话,可能会导致冲突或者死锁。请小心管理文件锁以避免出现此问题。一旦您成功地获取了一个文件锁,如果随后在通道上出现错误的话,请务必释放这个锁。推荐使用类似下面的代码形式:
FileLock lock = fileChannel.lock() try { //perform read/write/whatever on channel } catch (IOException) [ //handle unexpected exception } finally { lock.release() }
例 3-3 中的代码使用共享锁实现了reader进程,使用独占锁实现了writer进程,图 1-7 和图 1-8对此有诠释。由于锁是与进程而不是 Java 线程关联的,您将需要运行该程序的多个拷贝。先从一个writer和两个或更多的readers开始,我们来看下不同类型的锁是如何交互的。
/*
*例 3-3 共享锁同独占锁交互
*/
package com.ronsoft.books.nio.channels;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.io.RandomAccessFile;
import java.util.Random;
/**
* Test locking with FileChannel.
* Run one copy of this code with arguments "-w /tmp/locktest.dat"
* and one or more copies with "-r /tmp/locktest.dat" to see the
* interactions of exclusive and shared locks. Note how too many
* readers can starve out the writer.
* Note: The filename you provide will be overwritten. Substitute
* an appropriate temp filename for your favorite OS.
*
* Created April, 2002
* @author Ron Hitchens (ron@ronsoft.com)
*/
public class LockTest {
private static final int SIZEOF_INT = 4;
private static final int INDEX_START = 0;
private static final int INDEX_COUNT = 10;
private static final int INDEX_SIZE = INDEX_COUNT * SIZEOF_INT;
private ByteBuffer buffer = ByteBuffer.allocate (INDEX_SIZE);
private IntBuffer indexBuffer = buffer.asIntBuffer();
private Random rand = new Random();
public static void main (String [] argv) throws Exception {
boolean writer = false;
String filename;
if (argv.length != 2) {
System.out.println ("Usage: [ -r | -w ] filename");
return;
}
writer = argv [0].equals("-w");
filename = argv [1];
RandomAccessFile raf = new RandomAccessFile (filename, (writer) ? "rw" : "r");
FileChannel fc = raf.getChannel();
LockTest lockTest = new LockTest();
if (writer) {
lockTest.doUpdates(fc);
} else {
lockTest.doQueries(fc);
}
}
// ----------------------------------------------------------------
// Simulate a series of read-only queries while
// holding a shared lock on the index area
void doQueries (FileChannel fc) throws Exception {
while (true) {
println ("trying for shared lock...");
FileLock lock = fc.lock(INDEX_START, INDEX_SIZE, true);
int reps = rand.nextInt(60) + 20;
for (int i = 0; i < reps; i++) {
int n = rand.nextInt(INDEX_COUNT);
int position = INDEX_START + (n * SIZEOF_INT);
buffer.clear();
fc.read(buffer, position);
int value = indexBuffer.get (n);
println("Index entry " + n + "=" + value);
// Pretend to be doing some work
Thread.sleep(100);
}
lock.release();
println("");
Thread.sleep(rand.nextInt (3000) + 500);
}
}
// Simulate a series of updates to the index area
// while holding an exclusive lock
void doUpdates (FileChannel fc) throws Exception {
while (true) {
println("trying for exclusive lock...");
FileLock lock = fc.lock (INDEX_START, INDEX_SIZE, false);
updateIndex(fc);
lock.release();
println("");
Thread.sleep(rand.nextInt(2000) + 500);
}
}
// Write new values to the index slots
private int idxval = 1;
private void updateIndex (FileChannel fc) throws Exception {
// "indexBuffer" is an int view of "buffer"
indexBuffer.clear();
for (int i = 0; i < INDEX_COUNT; i++) {
idxval++;
println ("Updating index " + i + "=" + idxval);
indexBuffer.put (idxval);
// Pretend that this is really hard work
Thread.sleep (500);
}
// leaves position and limit correct for whole buffer
buffer.clear();
fc.write (buffer, INDEX_START);
}
// ----------------------------------------------------------------
private int lastLineLen = 0;
// Specialized println that repaints the current line
private void println (String msg) {
System.out.print ("\r ");
System.out.print (msg);
for (int i = msg.length(); i < lastLineLen; i++) {
System.out.print(" ");
}
System.out.print ("\r");
System.out.flush();
lastLineLen = msg.length();
}
}
以上代码直接忽略了我之前说给的用try/catch/finally来释放锁的建议,在您自己所写的实际代码中请不要这么懒。
0 条评论
撰写评论