ネットワークやファイルなどの入出力操作によって処理がブロックされるスレッドやタスクは、サービス運用妨害の脆弱性を防ぐために、明示的な終了の仕組みを呼出し元に提供する必要がある。
違反コード (ブロッキング I/O、volatile フラグ)
以下の違反コード例は、「THI05-J. スレッドの強制終了にThread.stop()メソッドを使用しない」に示しているように、volatile宣言したdoneフラグを使用してスレッドが終了可能であるかどうかを表している。しかし、readLine()メソッドを呼出すと、ネットワーク入出力処理が完了するまでスレッドは待機状態になり、新たに設定されたフラグの値に反応することができない。したがって、スレッドの終了は遅延させられるかもしれない。
// スレッドセーフなクラス
public final class SocketReader implements Runnable {
private final Socket socket;
private final BufferedReader in;
private volatile boolean done = false;
private final Object lock = new Object();
public SocketReader(String host, int port) throws IOException {
this.socket = new Socket(host, port);
this.in = new BufferedReader(
new InputStreamReader(this.socket.getInputStream())
);
}
// 一度に1つのスレッドだけがソケットを使用できる
@Override public void run() {
try {
synchronized (lock) {
readData();
}
} catch (IOException ie) {
// ハンドラへの転送
}
}
public void readData() throws IOException {
String string;
while (!done && (string = in.readLine()) != null) {
// ストリームの終り(null)までスレッドをブロックする
}
}
public void shutdown() {
done = true;
}
public static void main(String[] args)
throws IOException, InterruptedException {
SocketReader reader = new SocketReader("somehost", 25);
Thread thread = new Thread(reader);
thread.start();
Thread.sleep(1000);
reader.shutdown(); // スレッドを終了する
}
}
違反コード (割り込み可能なブロッキング I/O)
以下の違反コード例は前述のコード例に似ているが、スレッドを終了するためにスレッドへの割込みを使用している。しかし、java.net.Socketを使用したネットワーク入出力は、スレッドへの割込みに反応しない。
// スレッドセーフなクラス
public final class SocketReader implements Runnable {
// other methods...
public void readData() throws IOException {
String string;
while (!Thread.interrupted() && (string = in.readLine()) != null) {
// ストリームの終り(null)までスレッドをブロックする
}
}
public static void main(String[] args)
throws IOException, InterruptedException {
SocketReader reader = new SocketReader("somehost", 25);
Thread thread = new Thread(reader);
thread.start();
Thread.sleep(1000);
thread.interrupt(); // スレッドへの割込みを行う
}
}
適合コード (ソケット接続をクローズする)
以下の適合コードでは、shutdown()メソッドの中でソケットをクローズすることで、ネットワーク入出力を終了させている。readLine()メソッドは、ソケットをクローズする時にSocketException例外をスローし、スレッドの処理は継続する。ソケットを接続状態にしたまま、スレッドの処理を直ちに正しく停止することはできない。
public final class SocketReader implements Runnable {
// その他のメソッド...
public void readData() throws IOException {
String string;
try {
while ((string = in.readLine()) != null) {
// ストリームの終り(null)までスレッドをブロックする
}
} finally {
shutdown();
}
}
public void shutdown() throws IOException {
socket.close();
}
public static void main(String[] args)
throws IOException, InterruptedException {
SocketReader reader = new SocketReader("somehost", 25);
Thread thread = new Thread(reader);
thread.start();
Thread.sleep(1000);
reader.shutdown();
}
}
main()メソッドからshutdown()メソッドが呼ばれると、readData()メソッド内のfinallyブロックで再度shutdown()メソッドが呼ばれ、ソケットが再びクローズしようとする。ソケットが既にクローズされている場合、2度目の shutdown() は何もしない。
非同期入出力を行う場合、java.nio.channels.Selectorクラスのclose()あるいはwakeup()メソッドを呼び出して、ブロック状態のスレッドを復帰させることもできる。
待機状態から抜け出した後、さらに処理を実行する必要がある場合、処理の中断をbooleanフラグで示すこと。コードにそのようなフラグを追加する場合、shutdown()メソッドでフラグをfalseにセットし、スレッドが適切な処理を行ってwhileループから抜け出せるようにすること。
適合コード (割込み可能なチャネル)
以下の適合コードでは、Socket接続の代わりに、割込み可能チャネルjava.nio.channels.SocketChannelを使用している。ネットワーク入出力処理を行うスレッドが、データの読取り中にThread.interrupt()メソッドで割込まれた場合、そのスレッドは、ClosedByInterruptExceptionを受け取り、チャネルは即座にクローズされる。また、スレッドの割込み状態も更新される。
public final class SocketReader implements Runnable {
private final SocketChannel sc;
private final Object lock = new Object();
public SocketReader(String host, int port) throws IOException {
sc = SocketChannel.open(new InetSocketAddress(host, port));
}
@Override public void run() {
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
synchronized (lock) {
while (!Thread.interrupted()) {
sc.read(buf);
// ...
}
}
} catch (IOException ie) {
// ハンドラへ処理を移す
}
}
public static void main(String[] args)
throws IOException, InterruptedException {
SocketReader reader = new SocketReader("somehost", 25);
Thread thread = new Thread(reader);
thread.start();
Thread.sleep(1000);
thread.interrupt();
}
}
この手法を使うことで、実行中のスレッドを割り込みをかけることができる。しかしこのコードでは、Thread.interrupted()メソッドを呼び出してスレッドの割り込みステータスをポーリングし、割込みが行われていた場合にスレッドを終了している。通常、リード処理によってスレッドは待機状態になるが、SocketChannelを使用することで、割込みが受信されるやいなやwhileループの条件が評価されるようになる。同様に、java.nio.channels.Selectorで待機状態になっているスレッドの interruput() メソッドを呼び出すことで、待機状態にあるスレッドの処理を再開させることができる。
違反コード (データベース接続)
スレッドごとに1つのJDBC(Java Database Connectivity)接続を作成するスレッドセーフなDBConnectorクラスの例を、以下の違反コードに示す。各JDBC接続は1つのスレッドに所属し、他のスレッドとは共有されない。JDBC接続はシングルスレッド向けであり、このコード例は一般的な使用例である。
public final class DBConnector implements Runnable {
private final String query;
DBConnector(String query) {
this.query = query;
}
@Override public void run() {
Connection connection;
try {
// 簡素化のため、ユーザ名とパスワードはハードコーディングしている
connection = DriverManager.getConnection(
"jdbc:driver:name",
"username",
"password"
);
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(query);
// ...
} catch (SQLException e) {
// ハンドラへ処理を移す
}
// ...
}
public static void main(String[] args) throws InterruptedException {
DBConnector connector = new DBConnector("suitable query");
Thread thread = new Thread(connector);
thread.start();
Thread.sleep(5000);
thread.interrupt();
}
}
データベース接続はソケットと同様、本質的に割込み可能ではない。したがって、このような設計では、テーブル結合のような、処理に時間のかかるクエリによってスレッドの他の処理がブロックされた場合に、クライアントはリソースをクローズしてタスクをキャンセルすることができない。
適合コード (Statement.cancel() メソッド)
以下の適合コードでは、データベース接続をThreadLocalクラスでラップし、initialValue()メソッドを呼ぶスレッドがデータベース接続のインスタンスを一意に取得できるようにしている。この手法の利点は、cancelStatement()メソッドを用意することで、他のスレッドやクライアントが必要に応じて、処理に時間のかかるクエリの実行を中断できることにある。cancelStatement()メソッドは、中でStatement.cancel()メソッドを呼び出している。
public final class DBConnector implements Runnable {
private final String query;
private volatile Statement stmt;
DBConnector(String query) {
this.query = query;
if (getConnection() != null) {
try {
stmt = getConnection().createStatement();
} catch (SQLException e) {
// ハンドラへの転送
}
}
}
private static final ThreadLocal<Connection> connectionHolder =
new ThreadLocal<Connection>() {
Connection connection = null;
@Override public Connection initialValue() {
try {
// ...
connection = DriverManager.getConnection(
"jdbc:driver:name",
"username",
"password"
);
} catch (SQLException e) {
// ハンドラへ処理を移す
}
return connection;
}
};
public Connection getConnection() {
return connectionHolder.get();
}
public boolean cancelStatement() { // クライアントによるキャンセルを許可する
if (stmt != null) {
try {
stmt.cancel();
return true;
} catch (SQLException e) {
// ハンドラへ処理を移す
}
}
return false;
}
@Override public void run() {
try {
if (stmt == null || (stmt.getConnection() != getConnection())) {
throw new IllegalStateException();
}
ResultSet rs = stmt.executeQuery(query);
// ...
} catch (SQLException e) {
// ハンドラへ処理を移す
}
// ...
}
public static void main(String[] args) throws InterruptedException {
DBConnector connector = new DBConnector("suitable query");
Thread thread = new Thread(connector);
thread.start();
Thread.sleep(5000);
connector.cancelStatement();
}
}
データベース管理システム(DBMS)およびドライバが両方ともキャンセル処理に対応していれば、Statement.cancel()メソッドはクエリをキャンセルできる。そうでなければ、クエリをキャンセルすることはできない。
Java API仕様にはStatementインタフェースについて以下のように記述している。[API 2006]
デフォルトでは、Statementオブジェクトごとに一つのResultSetオブジェクトだけが同時にオープンできる。したがって、一つのResultSetオブジェクトの読取りと並行して、別のResultSet読取りが行われる場合、それぞれ異なったStatementオブジェクトによって生成されていなければならない。
上記の適合コードでは、1つのResultSetオブジェクトだけが、1つのインスタンスに属するStatementオブジェクトに関連付けられるようにしている。したがって、1つのスレッドだけが、クエリの実行結果にアクセスできる。
リスク評価
スレッドを終了する仕組みを提供しないと、プログラムの応答が無くなったり、サービス運用妨害を引き起こす可能性がある。
| ルール | 脅威度 | 可能性 | 修正コスト | 優先度 | レベル |
|---|---|---|---|---|---|
| THI04-J | 低 | 中 | 中 | P4 | L3 |
参考文献
| [API 2006] | Class Thread, method stop, interface ExecutorService |
| [Darwin 2004] | 24.3, Stopping a Thread |
| [JDK7 2008] | Java Thread Primitive Deprecation |
| [JPL 2006] | 14.12.1, Don't Stop; 23.3.3, Shutdown Strategies |
| [JavaThreads 2004] | 2.4, Two Approaches to Stopping a Thread |
| [Goetz 2006] | Chapter 7, Cancellation and Shutdown |
翻訳元
これは以下のページを翻訳したものです。
THI04-J. Ensure that threads performing blocking operations can be terminated (revision 86)
