アクセス制御を実現する便利なクラス
続いて、J2SE 1.4で導入された便利なAPIを紹介しましょう。
皆さんは、Javaアプリケーションで利用するファイルのアクセス制御をどのように実現しているでしょうか。最も一般的なのは、通知ロック(advisory lock)と呼ばれる方法を使うことでしょう。
通知ロックとは、ファイルを開く際にロック・ファイルを生成する処理を組み込み(リスト6)、ロック・ファイルが存在する間は、他のプログラムからそのファイルへアクセスすることを許さない(ロック状態にする)というものです。そして、ファイルに対する処理が終了した後に、ロック・ファイルを削除することで、他のプログラムからもそのファイルにアクセスできるようにします。
| リスト6:通知ロックを実現するプログラム |
 |  |  |  |
| import java.io.*; |  | |
|
|
| public class LegacyLockTest { |  | |
|
| public static void main(String[] args) throws Exception { |  | |
|
| String filename = args[0]; |  | |
|
| File lock = new File(filename + ".pid"); |  | |
|
| if (lock.createNewFile()) { |  | |
|
| // ファイルに対する処理など |  | |
|
| lock.delete(); |  | |
|
| } else { |  | |
|
| // ファイルがロック中のため処理できません |  | |
|
| } |  | |
|
| } |  | |
|
| } |  | |
|
|  |  |  |  |
|
従来のJavaアプリケーションでは、このような処理をプログラム中に直接書き込むことでアクセス制御を実現していたため、このルールを知らないプログラマーが修正を加えた場合に、アクセス制御がおかしくなってしまうことがありました。また、第三者がロック・ファイルを手作業で削除することが可能であるため、ソース・コードや実行ログからは追跡できない原因不明の障害が発生することもありました。
こうした問題を回避するために、J2SE 1.4で、クラスjava.nio.channels.FileLockが導入されました。このクラスは、その名のとおり、ファイルに対するロック機構を提供するというものです。ファイル操作を行うアプリケーションでは、ファイルのロック機構が頻繁に必要となるので、このようなクラスがそれまで標準APIとして提供されていなかったのが不思議なくらいだと言えるでしょう。
クラスFileLockの利用例
それでは、クラスFileLockを使った簡単なプログラムをご覧いただきます(リスト7)。
| リスト7:クラスFileLockの使用例 |
 |  |  |  |
| import java.nio.*; |  | |
|
| import java.nio.channels.*; |  | |
|
| import java.io.*; |  | |
|
|
| public class BasicLockTest { |  | |
|
| public static void main(String[] args) throws Exception { |  | |
|
| String filename = args[0]; |  | |
|
|  |  |  |  | | | RandomAccessFile file = new RandomAccessFile(filename, "rw"); | | | |  | (1) |
| FileChannel channel = file.getChannel(); | | | |  | | |  |  |  |  | |
| | FileLock lock = channel.lock(); |  |  (2) |
|
| lock.release(); |  |  (3) |
|
| channel.close(); |  |  (4) |
|
| } |  | |
|
| } |  | |
|
|  |  |  |  |
|
このプログラムでは、まず、クラスjava.io.RandomAccessFileのメソッドgetChannelを使って、java.nio.channels.FileChannelオブジェクトを取得しています(リスト7-(1))。クラスFileLockを使用する際には、このオブジェクトが必要になります。
クラスFileChannelは、インタフェースjava.nio.channels.Channelをimplementsした抽象クラスjava.nio.channels.spi.AbstractInterruptibleChannelのサブクラスです。インタフェースChannelは、簡単に言えば、「ファイルやネットワークに対する入出力を実現するためのインタフェース」です。
クラスFileChannelは、抽象クラスとして定義されています。したがって、クラスFileChannel型のインスタンスは、キーワードnewを使って生成することはできません。このクラスのインスタンスを生成するには、クラスRandomAccessFileやjava.io.FileInputStream、java.io.FileOutputStreamのインスタンスから、FileChannelオブジェクト生成用のメソッドを呼び出して取得する必要があります。
さて、リスト7の説明に戻りましょう。ファイルをロックするには、FileChannelオブジェクトのメソッドlockを呼び出して、FileLockオブジェクトを取得します(リスト7-(2))。このFileLockオブジェクトを取得している間が、ロックを保持した状態になります。
ロックを解放するには、FileLockオブジェクトのメソッドreleaseを呼び出します(リスト7-(3))。ロックを解放した後は、FileChannelオブジェクトのメソッドcloseを呼び出し、同オブジェクトをクローズする必要があります(リスト7-(4))。なお、ロックの解除を行わずにFileChannelオブジェクトをクローズした場合にも、ロックは解放されます。
続いて、リスト8のクラスLockTestをご覧ください。これは、ロック状態を作り出す、アクセス制御用のユーティリティ・クラスです。
| リスト8:アクセス制御用のユーティリティ・クラス |
 |  |  |  |
| import java.io.*; |  | |
|
| import java.nio.*; |  | |
|
| import java.nio.channels.*; |  | |
|
| import javax.swing.*; |  | |
|
|
| public final class LockTest { |  | |
|
| public static void runSharedLock(String filename) |  | |
|
| throws FileNotFoundException, IOException { |  | |
|
| runLock(filename, true); |  | |
|
| } |  | |
|
|
| public static void runExclusiveLock(String filename) |  | |
|
| throws FileNotFoundException, IOException { |  | |
|
| runLock(filename, false); |  | |
|
| } |  | |
|
|
| private static void runLock(String filename, boolean shared) |  | |
|
| throws FileNotFoundException, IOException { |  | |
|
| RandomAccessFile file = new RandomAccessFile(filename, "rw"); |  | |
|
| FileChannel channel = file.getChannel(); |  | |
|
| FileLock lock = channel.lock(0, Long.MAX_VALUE, shared); |  | |
|
|
| Object[] options = {"解除"}; |  | |
|
| JOptionPane.showOptionDialog( |  | |
|
| null, filename + "をロックしています。", "ロック中", |  | |
|
| JOptionPane.YES_OPTION, JOptionPane.PLAIN_MESSAGE, |  | |
|
| null, options, options[0]); |  | |
|
|
| lock.release(); |  | |
|
| channel.close(); |  | |
|
| } |  | |
|
| } |  | |
|
|  |  |  |  |
|
このクラスLockTestでは、ロック獲得用のメソッドを2つ定義しています。
1つは、runSharedLockというメソッドです。このメソッドは、クラスFileChannelのメソッドlock(long position, long size, boolean shared)の引数に「0, Long.MAX_VALUE, true」を設定したのと同じものとして機能します。メソッドlockによって提供されるロック機構は、「ロックを取得できなかった場合は、ロックを取得できるまで待機する」という動作を実現し、これを利用したメソッドrunSharedLockは、アクセスの対象となるファイルが読み込み可能な場合に読み込み専用のロックを取得する「共有ロック」の機能を提供します。
もう1つはrunExclusiveLockというメソッドです。このメソッドは、メソッドlock(0, Long.MAX_VALUE, false)を呼び出すのと同じものとして機能します。具体的には、アクセスの対象となるファイルが書き込み可能な場合に、書き込み処理も行えるロックを取得する「排他ロック」の機能を提供します。なお、メソッドlock(0, Long.MAX_VALUE, false)は、メソッドlock()と同様の処理を行います。
両メソッドとも、メソッドlock(long position, long size, boolean shared)の引数に「0, Long.MAX_VALUE, trueもしくはfalse」を設定すれば実現できるものですが、メソッドを分けてそれぞれにふさわしい名称を付けたことで使いやすくなっています。また、ロックの獲得を行う際に、煩わしいFileLockオブジェクトの生成処理の記述を省略できることがご理解いただけるでしょう。
それでは、実際にこれらのメソッドの動作を確認してみましょう。
リスト9のクラスSharedLockTestは、メソッドrunSharedLockを使ったプログラムの例です。一方、リスト10のクラスExclusiveLockTestは、メソッドrunExclusiveLockを使ったプログラムの例です。
| リスト9:メソッドrunSharedLockの使用例 |
 |  |  |  |
| public class SharedLockTest { |  | |
|
| public static void main(String[] args) throws Exception { |  | |
|
| LockTest.runSharedLock("./target.txt"); |  | |
|
| } |  | |
|
| } |  | |
|
|  |  |  |  |
|
| リスト10:メソッドrunExclusiveLockの使用例 |
 |  |  |  |
| public class ExclusiveLockTest { |  | |
|
| public static void main(String[] args) throws Exception { |  | |
|
| LockTest.runExclusiveLock("./target.txt"); |  | |
|
| } |  | |
|
| } |  | |
|
|  |  |  |  |
|
これらのプログラムの動作を確認する手順は、以下のようになります。
(1)コンソール(コマンド・プロンプト)を2つ立ち上げる。ここでは、便宜上、一方のコンソールを「Aコンソール」、もう一方のコンソールを「Bコンソール」と呼ぶことにする
(2)Aコンソール上でどちらかのプログラム(クラスSharedLockTestもしくはExclusiveLockTest)を実行する。すると、小さなウィンドウ(Swingアプリケーション)が起動するので、それを閉じずにそのままの状態にしておく。この状態が、Aコンソールのプログラムがロックを保持している状態である
(3)Bコンソール上でもう一方のプログラムを実行する。その際、ロックを取得できた場合は、新たにSwingアプリケーションが起動される。それに対し、ロックを取得できなかった場合は、(2)で立ち上げたSwingアプリケーションを閉じるまで、新たなウィンドウは起動されない
Aコンソール、Bコンソールで実行するプログラムの組み合わせを変えて、上記の操作を行った結果をまとめると、表1のようになります。注目すべきは、表1の2行目の実行結果です。この結果から、共有ロック(読み込み専用のロック)を獲得したアプリケーションが実行されている間は、排他ロック(書き込み可能なロック)も獲得できないことがわかります。さらに、3行目の結果から、排他ロックを獲得したアプリケーションが実行されている場合は、共有ロックを獲得できないこともおわかりいただけるでしょう。
表1:クラスSharedLockTest/ExclusiveLockTestの実行結果
| Aコンソール |
Bコンソール |
実行結果 |
| クラスSharedLockTest |
クラスSharedLockTest |
ロックの取得が可能(Bコンソールのアプリケーションがすぐに起動される) |
| クラスSharedLockTest |
クラスExclusiveLockTest |
ロックの取得は不可(Bコンソールのアプリケーションは待機状態になる) |
| クラスExclusiveLockTest |
クラスSharedLockTest |
ロックの取得は不可(Bコンソールのアプリケーションは待機状態になる) |
| クラスExclusiveLockTest |
クラスExclusiveLockTest |
ロックの取得は不可(Bコンソールのアプリケーションは待機状態になる) |
また、Aコンソールでプログラムを実行し、Swingアプリケーションが起動した状態で、テキスト・エディタを使ってターゲットとなるファイル(target.txt)を編集して保存しようとすると、「上書き保存ができない」という旨のエラー・メッセージが表示されます。
なお、クラスFileLockのアクセス制御は、基本的にOSネイティブのロック機構を用いて実現されています。したがって、クラスFileLockを実行した結果、アクセス制御がどのように実現されるのかはプラットフォームに依存するので注意してください。
メソッドtryLock
FileLockオブジェクトの生成用メソッドとしては、上で紹介したメソッドlock以外に、tryLockというものも用意されています。メソッドlockとの違いは、ロックを取得できなかった場合に、待機することなく、すぐにnullを返す点です。
それでは、メソッドtryLockの動作を確認してみましょう。
リスト11に示したのは、メソッドtryLockを使った簡単なプログラムです。
| リスト11:メソッドtryLockの使用例 |
 |  |  |  |
| import java.io.*; |  | |
|
| import java.nio.*; |  | |
|
| import java.nio.channels.*; |  | |
|
|
| public class TryToLockTest { |  | |
|
| public static void main(String[] args) throws Exception { |  | |
|
| RandomAccessFile file |  | |
|
| = new RandomAccessFile("./target.txt", "rw"); |  | |
|
| FileChannel channel = file.getChannel(); |  | |
|
| FileLock lock = channel.tryLock(); |  | |
|
|
| if (lock == null) { |  | |
|
| System.out.println( |  | |
|
| "ほかのプログラムによって、ターゲットのファイルはロックされています"); |  | |
|
| } else { |  | |
|
| System.out.println("ロック取得成功"); |  | |
|
| } |  | |
|
|
| lock.release(); |  | |
|
| channel.close(); |  | |
|
| } |  | |
|
| } |  | |
|
|  |  |  |  |
|
このクラスTryToLockTestを実行すると、以下のような結果が出力されます。
 |
| ロック取得成功 |
 |
ご覧のとおり、ロックを取得することに成功しています。
続いて、リスト10のクラスExclusiveLockTestを実行しておき、ロックを獲得できない状態でクラスTryToLockTestを実行してみましょう。すると、以下のような実行結果がすぐに出力されます。
 |
| ほかのプログラムによって、ターゲットのファイルはロックされています |
 |
リスト11をご覧になればわかるとおり、このメッセージは、FileLockオブジェクトにnullが格納されている場合に出力されるメッセージです。このことから、メソッドtrylockを使って生成したFileLockオブジェクトは、ロックを獲得できなかった場合でも待機することなく、すぐにnullを返すことがおわかりいただけるでしょう。
なお、クラスTryToLockTestで呼び出しているメソッドtryLock()は、メソッドtryLock(0, Long.MAX_VALUE, false)と同様に動作します。
以上、今回は、Fileオブジェクトの扱い方や、アクセス制御を実現するクラスについて説明しました。
アプリケーションからファイルにアクセスした後には、読み込みや書き込みといった処理を行うことになるはずです。次回は、それらの処理について解説する予定です。
本稿は、月刊JavaWorld 2005年9月号掲載の連載「Java API學習塾 第5回」を再編集したものです。