レベル: 上級 Allen Holub, Contributing Editor, JavaWorld
2000年 10月 01日
Allen
Holubは、Javaプログラム言語にとって最大の弱点は、そのスレッド化のモデルであろうと述べています。現在のスレッド化のモデルでは、実用となる程度に複雑なプログラムにとっては、まったく不十分であり、そして、少しもオブジェクト指向ではないのです。この記事では、この種の多くの問題の解決に有効であると思われる、Java言語への変更と追加を提案します。
Javaのスレッド化モデルは、この言語の最も満足のいかない点の1つです。スレッド化が組み込まれているのは良いことですが、構文レベルおよびパッケージ・レベルでのスレッドのサポートが最小限に過ぎないので、非常に小さなアプリケーションでもない限り、何の役にも立ちません。
Javaのスレッド化について論じた書籍の大部分は、Javaのスレッド化モデルの不都合な点を並べ立て、そうした問題の一時しのぎとなるクラス・ライブラリーを紹介しています。そうしたクラスを「一時しのぎ」と呼ぶのは、そうしたクラスが解決する問題は、本来Java言語の構文の一部であるべきだからです。ライブラリーの代わりに構文的な手法を用いるなら、長い目で見て、より良いコードが生み出されることになります。なぜなら、コンパイラーとJVMが協働して、ライブラリーによるアプローチでは困難あるいは不可能な最適化を実行できるからです。
Taming Java Threads と題するわたしの著作 (
参考文献
参照)、およびこの記事では、さらに一歩進んで、スレッド化の問題に関するより積極的なアプローチを提案します。それは、Javaプログラム言語に対し、本当の解決策となるいくつかの変更を加えるというものです。この記事とわたしの著作との間の主な相違は、この問題について考察を進めた結果、提案がもう少し改善されたという点にあります。これらの提案は暫定的なものに過ぎません。一人物による問題の考察に過ぎず、これを実行に移すには、多くの作業と専門家の意見を必要とすることでしょう。しかし、これははじまりとなるものです。わたしは、この問題に関するワーキング・グループを設立するつもりでいます。関心がおありになる方は、
threading@holub.com
あてに電子メールをお送りください。実現に向けて物事が動き出したら、ご連絡を差し上げます。
ここに示す提案は、いくぶん大胆なものでもあります。これまで、何人かが、現在あいまいなものとなっているJVMの動作を修正するために、Java
Language Specification (JLS) (
参考文献
参照) に対する軽微で最小限の変更を提案してきました。しかし、わたしが望んでいるのは、より徹底的な改善です。
実際的に言えば、ここに示す提案の多くには、新しいキーワードを言語に導入することが伴います。既存のコードを壊したくないという通常の要件は大変もっともなものです。しかし、言語が停滞して時代遅れにならないようにするには、キーワードの導入が可能でなければなりません。導入するキーワードが既存のIDと対立しないよう、意図的にIDの中では無許可の文字
(
$
) を使用しました (たとえば、
task
の代わりに
$task
)。コンパイラーのコマンド行スイッチを使用すれば、おそらく、これらのキーワードのドル記号を省略したバリアントも使用できるようになるでしょう。
タスク の概念
Javaのスレッド化モデルの基本的な問題は、それが少しもオブジェクト指向ではないという点です。OOの設計担当者は、スレッドについてまったく考えないものです。むしろ、同期
メッセージ (即座に処理されるメッセージ。メッセージ・ハンドラーは、メッセージが完了するまで戻りません)
と、非同期 メッセージ
(受け取られた後、しばらくしてからバックグラウンドで処理されるメッセージ。メッセージ・ハンドラーは即座に戻り、メッセージが完了するのはそれよりずっと後になります)
について考えます。Javaプログラム言語の
Toolkit.getImage()
メソッドは、非同期メッセージの良い例です。
getImage()
メッセージ・ハンドラーは即座に戻り、その後で、イメージ全体がバックグラウンド・スレッドによって取り出されます。
これがOOのアプローチですが、先にも述べたとおり、Javaのスレッド化モデルはOOではありません。Javaプログラム言語のスレッドは、実際のところ、他のプロシージャーを呼び出すプロシージャー
(
run()
)
に過ぎません。オブジェクトの概念や、非同期メッセージと同期メッセージの対比の概念などは、まったく考慮されていません。
この問題の解決策の1つは、わたしの著作の中で詳しく説明した、
Active_object
です。アクティブ・オブジェクトは、非同期要求を受け取ることのできるオブジェクトです。非同期要求は、受け取られた後、しばらくしてから処理されます。Javaプログラム言語では、要求をオブジェクトにカプセル化することができます。たとえば、行うべき作業を
run()
メソッドにカプセル化した
Runnable
インプリメンテーションのインスタンスを、アクティブ・オブジェクトに渡すことができます。この実行可能オブジェクトはアクティブ・オブジェクトによってキューに入れられ、オブジェクトに余裕ができたらバックグラウンド・スレッドで実行されます。
アクティブ・オブジェクト上で実行される非同期メッセージ群は、実際のところ、互いの関係においては同期的です。というのは、それらのメッセージは、キューから取り出されて、単一のサービス・スレッドにより、一度に1つずつ実行されるからです。したがって、アクティブ・オブジェクトを使用すれば、手続き型モデルの度合いがより強いプログラムで必要とされる、同期の困難の多くがなくなります。
Javaプログラム言語のSwing/AWTサブシステム全体も、ある意味でアクティブ・オブジェクトであると言えます。Swingクラスにメッセージを送るための唯一の安全な方法は、
SwingUtilities.invokeLater()
のようなメソッドを呼び出すことです。これにより、実行可能オブジェクトがSwingのイベント・キューに入れられます。このオブジェクトは、Swingのイベント処理スレッドに余裕ができた時点で、このスレッドによって処理されます。
したがって、わたしの最初の提案は、Javaプログラム言語にタスク
の概念を取り込むことにより、言語そのものにアクティブ・オブジェクトを取り込むというものです。(タスクの概念は、IntelのRMXオペレーティング・システムおよびAdaプログラミング言語から借用しました。リアルタイムのオペレーティング・システムの大部分は、類似の概念をサポートしています。)
タスクは、組み込みアクティブ・オブジェクト・ディスパッチャーを持ち、非同期メッセージを自動的に処理するための機構すべての面倒を見ます。
タスクの定義はクラスの場合とまったく同様です。ただし、タスクのメソッドには、そのメソッドがアクティブ・オブジェクト・ディスパッチャーでバックグラウンドに実行されることを示す、
asynchronous
修飾子を適用できるという点が異なります。わたしの著作の第9章で説明したクラスに基づくアプローチとの類似点を見るために、以下のファイル入出力クラスを考慮してください。ここでは、Taming
Java Threads で説明した
Active_object
クラスを使用して、非同期の書き込み操作をインプリメントしています。
interface Exception_handler
{ void handle_exception( Throwable e );
}
class File_io_task
{ Active_object dispatcher = new Active_object();
final OutputStream file;
final Exception_handler handler;
File_io_task( String file_name, Exception_handler handler )
throws IOException
{ file = new FileOutputStream( file_name );
this.handler = handler;
}
public void write( final byte[] bytes )
{
// The following call asks the active-object dispatcher
// to enqueue the Runnable object on its request
// queue. A thread associated with the active object
// dequeues the runnable objects and executes them
// one at a time.
dispatcher.dispatch
( new Runnable()
{ public void run()
{
try
{ byte[] copy new byte[ bytes.length ];
System.arrayCopy( bytes, 0,
copy, 0,
bytes.length );
file.write( copy );
}
catch( Throwable problem )
{ handler.handle_exception( problem );
}
}
}
);
}
}
|
すべての書き込み要求は、
dispatch()
呼び出しにより、アクティブ・オブジェクトの入力キューに入れられます。非同期メッセージのバックグラウンド処理中に例外が起こった場合、それは
Exception_handler
オブジェクトによって処理されます。このオブジェクトは、
File_io_task
のコンストラクターに渡されます。次のようにファイルへの書き込みを行うことができます。
File_io_task io = new File_io_task
( "foo.txt"
new Exception_handler
{ public void handle( Throwable e )
{ e.printStackTrace();
}
}
);
//...
io.write( some_bytes ); |
クラスに基づくこのアプローチの主な問題点は、それがあまりに複雑であることです。簡単なことをするために、コードにたくさんの複雑な追加を行わなければなりません。言語に
$task
と
$asynchronous
の両キーワードを導入すれば、先のコードは次のように書き直せます。
$task File_io $error{ $.printStackTrace(); }
{
OutputStream file;
File_io( String file_name ) throws IOException
{ file = new FileOutputStream( file_name );
}
asynchronous public write( byte[] bytes )
{ file.write( bytes );
}
}
|
非同期メソッドは戻り値を指定しないということに注意してください。これは、ハンドラーが即座に戻り、要求された操作が完了するのはそのずっと後になるためです。その結果、戻すことのできる妥当な値はないことになります。
$task
キーワードは、導出モデルに関して言えば、
class
とまったく同様に機能します。
$task
は、インターフェースのインプリメント、クラスの拡張、および他のタスクの拡張を行うことができます。
asynchronous
キーワードでマークされたメソッドは、
$task
によってバックグラウンドで処理されます。それ以外のメソッドは、クラスの場合と同様に、同期的に機能します。
$task
キーワードは、(上に示したとおり) オプショナルの
$error
文節で修飾することができます。この文節は、非同期メソッド自身がキャッチできなかった例外のためのデフォルト・ハンドラーを指定します。
$
は、スローされた例外オブジェクトを表すために使用されています。
$error
文節が指定されていない場合は、妥当なエラー・メッセージ (おそらくはスタック・トレースも) がプリントされます。
非同期メソッドへの引数はスレッド・セーフであるという点で不変でなければならないということに注意してください。この不変性を保証するのに必要なセマンティクスについては、ランタイム・システムがすべてその面倒を見なければなりません。(簡単なコピーでは不十分な場合がしばしばあります。)
すべてのタスク・オブジェクトは、いくつかの疑似メッセージもサポートする必要があります。
some_task.close()
|
この呼び出しが出された後に送信されるあらゆる非同期メッセージは、
TaskClosedException
をスローします。しかし、アクティブ・オブジェクト・キューを待っているメッセージは、サービスを受けます。
|
some_task.join()
|
呼び出し元は、タスクが閉じられて、未処理要求がすべて処理されるまで、ブロックされます。
|
task
キーワードは、通常の修飾子 (
public
など) に加えて、
$pooled(n)
修飾子も受け入れます。この修飾子を指定すると、
task
は、非同期要求を実行するために、単一のスレッドではなく、スレッドのプールを使用するようになります。
n
には、望ましいプールのサイズを指定します。プールは必要なら大きくすることができますが、追加のスレッドが必要ないなら、元のサイズに戻してください。疑似フィールド
$pool_sizeは、元の
n
引数を
$pooled(n)
ディレクティブに戻します。
Taming Java Threads
の第8章で、スレッド・プールの使用例として、サーバー側のソケット・ハンドラーを示しました。これは、プールされるタスクのインプリメンテーションとして良い例です。基本的なアイデアは、サーバー側のソケットをモニターするというジョブを持つ、単一のオブジェクトを作成するというものです。クライアントが接続するたびに、ソケット・サーバーは、事前に作成された休止状態のスレッドのプールからスレッドを1つ選び、クライアント接続のサービスを行うようそのスレッドを設定します。クライアントの接続試行数が、事前に作成してあるスレッドの数を超えた場合、ソケット・サーバーは追加のクライアント・サービス・スレッドを作成します。しかし、それらの追加のスレッドは、接続が閉じると廃棄されます。次に提案する構文に従って、ソケット・サーバーをインプリメントすることができるでしょう。
public $pooled(10) $task Client_handler
{
PrintWriter log = new PrintWriter( System.out );
public asynchronous void handle( Socket connection_to_the_client )
{ log.println("writing");
// client-handling code goes here. Every call to
// handle() is executed on its own thread, but 10
// threads are pre-created for this purpose. Additional
// threads are created on an as-needed basis, but are
// discarded when handle() returns.
}
} $task Socket_server
{
ServerSocket server;
Client_handler client_handlers = new Client_handler();
public Socket_server( int port_number )
{ server = new ServerSocket(port_number);
}
public $asynchronous listen(Client_handler client)
{ // This method is executed on its own thread.
while( true )
{ client_handlers.handle( server.accept() );
}
}
}
//...
Socket_server = new Socket_server( the_port_number );
server.listen() |
Socket_server
オブジェクトは、単一のバックグラウンド・スレッドを使用して、非同期の
listen()
要求を処理します。この要求は、ソケットの「受け入れ」ループをカプセル化しています。クライアントが接続するたび、
listen()
は、
handle()
を呼び出して要求を処理するよう、
Client_handler
に依頼します。各
handle()
要求は (
$pooled
タスクなので)、それぞれ独自のスレッドで実行されます。
$pooled $task
に送信されるすべての非同期メッセージが、事実上、それぞれ独自のスレッドで処理されることに注意してください。
$pooled $task
の典型的な使用法は自律式オペレーションをインプリメントするというものであるため、状態変数のアクセスに関係した、ひどいものとなり得る問題を処理する最良の方法は、おそらく、
$asynchronous
メソッドの
this
参照に、オブジェクトの固有のコピーを参照させるというものです。つまり、
$pooled $task
に非同期要求を送信すると、
clone()
オペレーションが実行され、メソッドの
this
参照はクローンを参照します。スレッド同士の通信は、
static
フィールドへの同期的なアクセスによって行われます。
synchronized
の改善点
多くの状況では
$task
によって同期の必要がなくなるとは言え、すべてのマルチスレッド・プログラムがタスクという観点だけでインプリメントできるわけではありません。したがって、既存のスレッド化モデルにも更新の必要があります。
synchronized
キーワードには、以下に挙げる幾つかの欠陥があります。
- タイムアウト値を指定できない。
- ロックの獲得を待っているスレッドを中断できない。
- 複数ロックを安全に獲得できない。(複数ロックはいつも同じ順序で獲得することが必要。)
これらの問題は、
synchronized
の既存の構文を拡張して、複数の引数のリストをサポートすることと、タイムアウトの指定 (以下の大括弧で指定)
を受け入れるようにすることとによって、解決できます。以下にわたしの提案する構文を示します。
synchronized(x && y && z)
|
x
、
y
、
z
オブジェクト
すべ
について、ロックを獲得します。
|
synchronized(x || y || z)
|
x
、
y
、
z
オブジェクトの
いずれか
について、ロックを獲得します。
|
synchronized( (x && y ) || z)
| 先のコードに加えた、一目瞭然な拡張。 |
synchronized(...)[1000]
| 単一のロックを獲得します。タイムアウトは1秒です。 |
synchronized[1000] f(){...}
|
f()
に入ると
this
のロックを獲得します。ただし、タイムアウトは1秒です。
|
TimeoutException
は
RuntimeException
から派生したもので、待機がタイムアウトになるとスローされます。
タイムアウトは必要なものですが、コードを堅固なものにするには十分ではありません。ロックの獲得の待機を、外部から終了できるようにすることも必要です。したがって、ロックの獲得を待機しているスレッドに
interrupt()
メソッドを送信すると、そのスレッドが獲得のブロックから抜けられるようにする必要があります。これは、
SynchronizationException
オブジェクトをトスすることによって行います。明示的な処理が不要になるよう、この例外は
RuntimeException
から派生したものでなければなりません。
ここに挙げた
synchronized
の構文への修正案の主な問題点は、バイトコード・レベルでの変更が必要になるということです。現在のところ、
synchronized
は入力モニター命令と終了モニター命令を使ってインプリメントされています。これらの命令は引数をとらないため、複数ロックの獲得をサポートするにはバイトコード定義の拡張が必要になります。もっとも、この変更は、Java
2においてJVMに加えられた変更に比べれば些細なものですし、既存のJavaコードへの逆方向への互換性も確保されるでしょう。
解決可能な別の問題は、最も一般的なデッドロックのシナリオ、つまり2つのスレッドが何かをするためにお互いを待つという状態です。以下の例
(不自然なものであることは一目瞭然ですが) を考慮してください。
class Broken
{ Object lock1 = new Object();
Object lock2 = new Object();
void a()
{ synchronized( lock1 )
{ synchronized( lock2 )
{ // do something
}
}
}
void b()
{ synchronized( lock2 )
{ synchronized( lock1 )
{ // do something
}
}
}
|
あるスレッドが
a()
を呼び出すと想像してください。しかし、
lock1
を獲得してから
lock2
を獲得するまでの間に、優先権を奪われてしまうとします。2番目のスレッドが登場して、
b()
を呼び出し、
lock2
を獲得します。しかし、
lock1
は、最初のスレッドが持っているので、入手することができません。そして、最初のスレッドが目覚めて、
lock2
を獲得しようとします。しかし、スレッド2がこれを持っているために、入手することができません。こうしてデッドロックが起きます。複数オブジェクトの同期を考慮した構文を使えば、この問題を次のように解決できます。
//...
void a()
{ synchronized( lock1 && lock2 )
{
}
}
void b()
{ synchronized( lock2 && lock3 )
{
}
}
|
コンパイラー (またはVM) が獲得の順序を再調整し、必ず
lock1
が最初に獲得されるようにします。こうして、デッドロックが回避されることになります。
しかし、複数の獲得をいつも使用できるとは限りません。それで、デッドロックを自動的に解除するための方法を備えておくのは良いことです。1つの簡単な戦略は、1つのロックを獲得した後、2番目のロックを待機している場合に、時々ロックを解除するというものです。これは、永久に待機する代わりに、次のように待機するという方法です。
while( true )
{ try
{ synchronized( some_lock )[10]
{ // do the work here.
break;
}
}
catch( TimeoutException e )
{ continue;
}
}
|
ロックを待機する各スレッドがそれぞれ微妙に異なるタイムアウト値を使えば、デッドロックは解除され、1つのスレッドが実行できるようになります。前に挙げたコードを表すものとして、次のような構文を提案します。
synchronized( some_lock )[]
{ // do the work here.
}
|
synchronized
ステートメントは永久に待ち続けるものの、生じ得るデッドロックを破るために、時々ロックを断念します。理想的なのは、各反復ごとに、使用するタイムアウトをランダムに変えることです。
wait()
と
notify()
の改善点
wait()
/
notify()
システムにも、以下の問題点があります。
-
wait()
が正常に戻されたのか、タイムアウトで戻されたのかを検出する手段がない。
- 「信号通知」状態にとどまる従来型の条件変数をインプリメントする手段がない。
- ネストされたモニターのロックアウトが非常に容易に発生し得る。
タイムアウトの検出の問題は、
wait()
が (
void
の代わりに)
boolean
を戻すように再定義することによって、容易に解決できます。
true
戻り値は正常に戻ったことを示し、
false
はタイムアウトを示すことになります。
状態に基づいた条件変数という概念は、重要なものです。変数が
false
状態に設定されることがあります。この場合、変数が
true
状態になるまで、待機中のスレッドはブロックされます。
true
の条件変数を待機しているスレッドはどれも、即座に解放されます。(この場合、
wait()
呼び出しはブロックされません)この機能性は、
notify()
の構文を次のように拡張することによってサポートできます。
| notify(); |
待機中のすべてのスレッドを、基礎となる条件変数の状態を変えずに解放します。
| | notify(true); |
条件変数の状態をtrueに設定し、待機中のスレッドがあれば、それをすべて解放します。
wait()
への後続の呼び出しは、ブロックされません。
| | notify(false); |
条件変数の状態をfalseに設定します (
wait()
への後続の呼び出しは、ブロックされます)。
|
ネストされたモニターのロックアウトの問題はより厄介なものであり、わたしは簡単な解決策を持ち合わせていません。ネストされたモニターのロックアウトはデッドロックの一種であり、あるロックを保持している側自体が、ロックを解放する前にサスペンドしてしまった状態です。以下に、この問題を示す一例
(これも不自然な例です) を挙げますが、実世界での例は枚挙にいとまがありません。
class Stack
{ LinkedList list = new LinkedList();
public synchronized void push(Object x)
{ synchronized(list)
{ list.addLast( x );
notify();
}
}
public synchronized Object pop()
{ synchronized(list)
{ if( list.size() <= 0 )
wait();
return list.removeLast();
}
}
}
|
問題は、
get()
オペレーションと
put()
オペレーションの両方に2つのロックが関係しているということです。一方は
Stack
オブジェクトに対するロック、もう一方は
LinkedList
に対するロックです。スレッドが空のスタックからポップしようとするなら何が起こるか考えてみてください。スレッドは両方のロックを獲得し、それから
wait()
を呼び出します。これにより、
Stack
オブジェクトに対するロックは解放されますが、リストに対するロックは解放されません。2番目のスレッドがオブジェクトをプッシュしようとすると、このスレッドは
synchronized(list)
ステートメントで永久にサスペンドし、決してオブジェクトをプッシュできるようにはなりません。最初のスレッドが待っている状態は空ではないスタックなのですから、これはデッドロックです。つまり、最初のスレッドは
wait()
から戻ることが決してできません。なぜなら、2番目のスレッドは決して
notify()
に到達できないからです。そして、2番目のスレッドがそのコードに到達できないのは、最初のスレッドがロックを保持しているからなのです。
この例については、すぐに分かる解決策がたくさんあります。たとえば、メソッド・レベルの同期について手を尽くすなどです。ただし、実世界では、解決策はそれほど単純でないのが普通です。
1つの可能な解決策は、現在のスレッドが獲得したすべての ロックを、獲得したときと逆の順番で
wait()
に解放させ、waitが完了したら、獲得したときの元の順番で再び獲得させるというものです。この動作を利用したコードは、およそ人間の考え付くものではないと思いますが、これが本当に実行可能な解決策であるとは思いません。何かアイデアのおありになる方は、電子メールを送ってください。
複合条件がtrueになるのを待機できるようにもしたいと思います。例を挙げます。
ここで、
a
、
b
、および
c
は任意の
Object
です。
Thread
クラスの修正
プリエンプティブ (優先権) スレッドとコーペラティブ (協調型)
スレッドの両方をサポートできることは、ある種のサーバー・アプリケーションにとって不可欠なことです。システムから最高のパフォーマンスを引き出そうとしている場合は、特にそうです。わたしは、Javaプログラム言語におけるスレッド化モデルの単純化は行き過ぎだと考えます。そして、Javaプログラム言語はPosix/Solarisにおける「グリーン・スレッド」や「軽量プロセス」の概念
(Taming Java Threads の第1章で説明しました)
をサポートすべきだと考えます。そうするなら当然、一部のJVMインプリメンテーション
(NTのインプリメンテーションなど)
では内部的にコーペラティブなスレッドをシミュレートしなければならず、その他のJVMではプリエンプティブなスレッド化をシミュレートしなければならなくなります。しかし、これらの拡張をJVMに追加することは大変容易なことです。
そして、Javaの
Thread
は、常にプリエンプティブなものとします。つまり、Javaプログラム言語のスレッドは、Solarisの軽量プロセスによく似た形で機能するということです。
Runnable
インターフェースは、Solaris流の「グリーン・スレッド」を定義するために使用することができます。グリーン・スレッドは、同じ軽量プロセス上で実行される他のグリーン・スレッドに制御を渡さなければなりません。
たとえば、次に挙げる現行の構文について考えてみます。
class My_thread implements Runnable
{ public void run(){ /*...*/ }
}
new Thread( new My_thread );
|
この構文を使って、実際に、
Runnable
オブジェクトのためにグリーン・スレッドを作成し、
Thread
オブジェクトで表される軽量プロセスにそのグリーン・スレッドをバインドできるようになります。このインプリメンテーションの変更は、既存のコードからは透過的なものです。なぜなら、実際の動作は現在の動作と同一だからです。
Runnable
オブジェクトをグリーン・スレッドと考えることにより、Javaプログラム言語の現在の構文を拡張して、単一の軽量プロセスに複数のグリーン・スレッドをバインドすることがサポートできるようになります。これは単に、幾つかの
Runnable
オブジェクトを
Thread
コンストラクターに渡すだけで行うことができます。(グリーン・スレッドは、お互い同士の間ではコーペラティブですが、他の軽量プロセス
(
Thread
オブジェクト) 上で実行される、その他のグリーン・スレッド (
Runnable
オブジェクト)
によって優先権を奪われる場合があります。)たとえば、次のコードは実行可能オブジェクトのそれぞれについてグリーン・スレッドを作成し、それらのグリーン・スレッドは、
Thread
で表されている軽量プロセスを共用します。
new Thread( new My_runnable_object(), new My_other_runnable_object() );
|
Thread
のオーバーライドや、
run()
のインプリメントなどの既存の語法は依然として有効ですが、それらは軽量プロセスにバインドされた単一のグリーン・スレッドにマップされなければなりません。(
Thread()
クラスのデフォルトの
run()
メソッドは、実際には内部的に2番目の
Runnable
オブジェクトを作成することになります。)
スレッド間の調整
スレッド間の通信をサポートするため、言語にさらに機能を追加しなければなりません。現在では、この目的のために、
PipedInputStream
クラスと
PipedOutputStream
クラスを使用できますが、これらのクラスは大部分のアプリケーションにとって非常に効率の悪いものです。
Thread
クラスに対して以下の追加を行うことを提案します。
-
wait_for_start()
メソッドを追加する。このメソッドは、スレッドの
run()
が開始するまでブロックされます。(runが呼び出される前に待機中のスレッドが解放されれば、問題ありません。)この方法により、1つのスレッドが1つ以上の補助スレッドを作成できるようになります。また、スレッドの作成から何かのオペレーションに移る前に、確実に補助スレッドが実行されているようにすることができるようになります。
-
$send(Object o)
および
Object=$receive()
メソッドを (
Object
に)
追加する。これらのメソッドは、スレッド同士のオブジェクトの受け渡しに内部ブロッキング・キューを使用します。ブロッキング・キューは、最初の
$send()
呼び出しの副次作用として、自動的に作成されます。
$send()
呼び出しはオブジェクトをエンキューします。
$receive()
呼び出しは、オブジェクトがエンキューされるまでブロックされ、その後でオブジェクトを戻します。このメソッドのバリアント
(
$send(Object o, long timeout)
と
$receive(long timeout)
) は、エンキューとデキューの両オペレーションについてタイムアウトをサポートします。
読み取りスレッドと書き込みスレッドのロックの内部的なサポート
読み取りスレッドと書き込みスレッドのロックの概念をJavaプログラム言語に組み込むことが必要です。読み取りスレッドと書き込みスレッドのロックについてはTaming
Java Threads (および他の資料)
で説明しましたが、要約すると次のようになります。すなわち、読み取りスレッドと書き込みスレッドのロックにより、複数のスレッドが同時にオブジェクトにアクセスすることはできるものの、オブジェクトに変更を加えられるのは一度に1つのスレッドに限られ、アクセスが行われている間に変更を加えることはできない、という規則が施行される、ということです。読み取りスレッドと書き込みスレッドのロックの構文は、
synchronized
キーワードの構文から借用できます。
static Object global_resource;
//...
public void a()
{
$reading( global_resource )
{ // While in this block, other threads requesting read
// access to global_resource will get it, but threads
// requesting write access will block.
}
}
public void b()
{
$writing( global_resource )
{ // Blocks until all ongoing read or write operations on
// global_resource are complete. No read or write
// operation or global_resource can be initiated while
// within this block.
}
}
public $reading void c()
{ // just like $reading(this)...
}
public $writing void d()
{ // just like $writing(this)...
}
|
複数のスレッドが
$reading
ブロックに入ることができます。ただしそれは、所定のオブジェクトに関し、
$writing
ブロックに入っているスレッドが何もない場合に限ります。あるスレッドが
$writing
ブロックに入ろうとしたとき、読み取りが実行中であると、そのスレッドはブロックされます。これは、読み取り中のスレッドが
$reading
ブロックから出るまで続きます。スレッドが
$reading
ブロックまたは
$writing
ブロックに入ろうとしたときに、別のスレッドが
$writing
ブロックに入っていた場合、前者のスレッドはブロックされます。これは、書き込み中のスレッドが
$writing
ブロックから出るまで続きます。
読み取りスレッドと書き込みスレッドの両方がアクセス待ちをしている場合、先にアクセスするのは、デフォルトでは読み取りスレッドです。しかし、このデフォルトを変更することもできます。それには、
$writer_priority
属性を使用して、
class
定義を変更します。例を挙げます。
$write_priority class IO
{ $writing write( byte[] data )
{ //...
}
$reading byte[] read( )
{ //...
}
} |
部分的に構成されたオブジェクトへのアクセスを許可しない
現在のところ、JLSでは、部分的に作成されたオブジェクトへのアクセスを認めています。たとえば、コンストラクターの中で作成されたスレッドは、作成中のオブジェクトにアクセスできます。たとえそのオブジェクトが完全に構成されていなくても、アクセスできます。以下のコードの動作は不確定なものとなります。
class Broken
{ private long x;
Broken()
{ new Thread()
{ public void run()
{ x = -1;
}
}.start();
x = 0;
}
}
|
x
を -1に設定するスレッドと、
x
を0に設定するスレッドが、並列に実行可能です。したがって、
x
の値は予測不能です。
この問題に対して考えられる解決策の1つは、コンストラクターの中から開始されるスレッドの
run()
メソッドを、コンストラクターが戻るまでは実行できないようにするというものです。たとえコンストラクターにより作成されるスレッドの優先順位が、
new
を呼び出したスレッドよりも高くても、そうします。
これは、
start()
要求を、コンストラクターが戻るまで据え置かなければならないということです。
あるいは、Javaプログラム言語にコンストラクターの同期を許可させるという方法もあります。言い換えれば、以下のコード
(現在では無許可) が期待どおりに機能するようになるということです。
class Illegal
{ private long x;
synchronized Broken()
{ new Thread()
{ public void run()
{synchronized( Illegal.this )
{
x = -1;
}
}
}.start();
x = 0;
}
}
|
最初のアプローチの方が2番目のものより分かりやすいものの、インプリメントについては明らかにこちらの方がより困難です。
volatile
キーワードをいつも期待どおりに機能させる
JLSでは、不安定さを持つオペレーションの順序を保持することが必要とされています。大部分のJVMのインプリメンテーションは、スペックのこの部分をまったく無視していますが、それは正しいことではありません。マルチプロセッサー状態で生じる類似の問題が多数あり、これらについてもJLSで対応する必要があります。スレッド化のこうした混乱状態に興味のある方には、University
of MarylandのBill Pughがこの方面について率先して尽力していることをお伝えします (
参考文献
参照)。
アクセスの問題
アクセス制御が良好でないと、スレッド化は必要以上に難しいものになります。同期のとれたサブシステムからだけ確実にメソッドを呼び出すことができるなら、メソッドをスレッド・セーフにする必要がなくなることがあります。Javaプログラム言語のアクセス権の概念を、次のように引き締めたいと思います。
-
パッケージ・アクセスを指定するのに、
package
キーワードを明示的に使用することを必須にする。わたしの考えでは、どのコンピューター言語であれ、デフォルトの動作があること自体が欠陥ですし、デフォルトのアクセス権さえ存在するということには当惑させられます
(デフォルトが「非公開」ではなく「パッケージ」であることには、なおさら当惑させられます)。Javaプログラム言語は、他の部分ではデフォルトのキーワードに相当するものを備えていません。明示的な
package
指定子を必須にすると既存のコードを壊すことになるのは事実ですが、コードはずっと読みやすくなりますし、(たとえば、アクセス権の脱落が故意ではなく過失ということになるなら)
あらゆる種類の潜在的なバグが取り除かれることになります。
-
private protected
を再び導入する。これは、現在の
protected
と同様に機能しますが、パッケージ・アクセスは許可しません。
-
外部のオブジェクトすべて (現在のオブジェクトと同じクラスのオブジェクトであっても) に対して
private
な「インプリメンテーション・アクセス」を指定するために、構文
private private
を許可する。ドット (暗黙的であれ明示的であれ) の左に指定することが許可される唯一の参照は、
this
です。
-
特定のクラスにアクセスを許可できるよう、
public
の構文を拡張する。たとえば、以下のコードは、クラス
Fred
に属するオブジェクトに、
some_method()
を呼び出すことを許可します。しかし、メソッドは、その他のオブジェクト・クラスに対しては非公開となります。
-
public(Fred) void some_method()
{
}
この提案は、C++ の「フレンド」機構とは異なります。フレンド機構は、別のクラスのすべての
非公開パーツに、クラスの全アクセスを許可するものです。ここでわたしが提案しているのは、限定されたメソッドのセットに対するアクセスを厳重に制御するということです。この方法により、あるクラスが、システムの残りの部分からは不可視である別のクラスとのインターフェースを定義することが可能になります。次のような、一目で理解できるバリアントもあります。
public(Fred, Wilma) void some_method()
{
}
-
真に不変のオブジェクトを参照しているのでない限り、あるいは
static final
基本データ型を定義しているのでない限り、すべてのフィールド定義を
private
とすることを必須とする。クラスのフィールドに直接アクセスできるということは、OO設計の基本的な2つの原則、すなわち抽象化とカプセル化に違反するものです。スレッド化の観点から言うと、フィールドへの直接アクセスを許可するなら、フィールドへの非同期アクセスを不注意に行う可能性が高まるに過ぎません。
-
$property
キーワードを追加する。この方法でタグ付けされたオブジェクトは、
Class
クラスで定義されたイントロスペクションAPIを使う、"bean box"
アプリケーションからアクセス可能です。しかし、その他の点では
private private
と同様に機能します。
$property
属性はフィールドとメソッドの両方に適用可能でなければなりません。このようにして、JavaBeanのgetter/setterメソッドがプロパティーとして容易に定義できるようになります。
不変性
不変性 (オブジェクトをいったん作成したら、その値を変更できない)
の概念は、マルチスレッドの状態ではとても貴重です。不変オブジェクトへの読み取り専用アクセスを同期化する必要がないからです。Javaプログラム言語の不変性の実装は、以下の2つの理由からして、十分に堅固なものとは言えません。
-
不変オブジェクトが、完全に作成されていないうちにアクセスされる可能性がある。こうしたアクセスにより、一部のフィールドの値が誤ったものとなる可能性があります。
-
不変の定義 (そのすべてのフィールドが
final
であるクラス) が、あまりに厳密さを欠く。
final
参照自体は状態を変えることができないのに、それによってアドレッシングされるオブジェクトの状態は変わる可能性があります。
最初の問題は、スレッドをコンストラクターの中から開始することを禁止することによって
(あるいは、コンストラクターが戻るまで開始要求を遅らせることによって)、解決されます。
2番目の問題は、
final
参照が不変オブジェクトを指すことを必須にすることにより、解決できます。つまり、オブジェクトが本当に不変となるのは、そのすべてのフィールドがfinalであることに加え、参照先のオブジェクトのすべてのフィールドもfinalである場合に限られるということです。既存のコードを破壊しないよう、この定義は、以下に示すように、クラスが明示的に不変としてタグ付けされている場合にだけ、コンパイラーによって施行することができるでしょう。
$immutable public class Fred
{ // all fields in this class must be final, and if the
// field is a reference, all fields in the referenced
// class must be final as well (recursively).
static int x constant = 0; // use of `final` is optional when $immutable
// is present.
}
|
$immutable
タグにより、フィールド定義での
final
の使用はオプショナルとすることができます。
最後に、インナー・クラスが現れた場合、Javaコンパイラーのバグにより、不変オブジェクトを確実に作成することが不可能になります。クラスに重要なインナー・クラスが含まれている場合
(わたしの作成するクラスの大部分はそうです)、コンパイラーが間違って次のエラー・メッセージをプリントすることがあります。
"Blank final variable 'name' may not have been initialized.
It must be assigned a value in an initializer, or in every constructor."
|
ブランクのfinalが確かに各コンストラクターによって初期化されている場合でも、このメッセージが現れることがあります。このバグはコンパイラーに含まれているものです。なぜなら、インナー・クラスが最初に導入されたのはバージョン1.1においてであり、(その3年後にあたる)
本稿執筆時においても、このバグがなお存在しているからです。そろそろこのバグを修正してもよいころです。
クラス・レベル・フィールドのインスタンス・レベル・アクセス
アクセス権に加えて、クラス・レベル (
static
) のメソッドとインスタンス (非
static
) のメソッドが両方とも、クラス・レベル (
static
)
のフィールドに直接アクセスできるという問題もあります。このアクセスは危険です。なぜなら、インスタンス・メソッドを同期化してもクラス・レベルのロックは得られず、
synchronized static
メソッドが、
synchronized
インスタンス・メソッドと同時にクラス・フィールドにアクセスできてしまうからです。この問題の明らかな解決策は、非不変な
static
フィールドへのアクセスを、インスタンス・メソッドから、
static
accessorメソッドを経由して行うことを必須とすることです。この要件により、当然、コンパイル時と実行時の検査が行われることになります。このガイドラインに沿って考えると、以下のコードは無許可のものとなります。
class Broken
{
static long x;
synchronized static void f()
{ x = 0;
}
synchronized void g()
{ x = -1;
}
};
|
これは、
f()
と
g()
が並列実行でき、
x
を同時に変更することができるためです
(結果は不確定です)。ここには2つのロックがあるということを思い出してください。すなわち、
static
メソッドは
Class
オブジェクトに関連したロックを獲得し、非静的メソッドはインスタンスに関係したロックを獲得します。コンパイラーには、次のどちらかがあてはまることになります。1つは、インスタンス・メソッドから非不変な
static
フィールドにアクセスするとき、以下の構造体を必要とするということです。
class Broken
{
static long x;
synchronized private static accessor( long value )
{ x = value;
}
synchronized static void f()
{ x = 0;
}
synchronized void g()
{accessor( -1 );
}
}
|
もう1つは、読み取りスレッド/書き込みスレッドのロックの使用が必要になるということです。
class Broken
{
static long x;
synchronized static void f()
{ $writing(x){ x = 0 };
}
synchronized void g()
{ $writing(x){ x = -1 };
}
}
|
代替手段として
(これが理想的な解決策ですが)、コンパイラーが非不変な静的フィールドへのアクセスを、読み取りスレッド/書き込みスレッドのロックを使用して自動的に
同期させて、プログラマーがこれについて心配しないでよいようにするという方法もあります。
デーモン・スレッドの突然のシャットダウン
デーモン・スレッドは、非デーモン・スレッドがすべて終了すると、突然にシャットダウンします。これは、デーモンがある種のグローバル・リソース
(データベース接続または一時ファイルなど)
を作成していたものの、終了時にそのリソースを閉じたり破棄したりしなかった場合に問題となります。
次のいずれかの場合にはJVMがアプリケーションをシャットダウンしないという規則を作ることによって、この問題を解決したいと思います。
- 実行中の非デーモン・スレッドがある場合。
-
synchronized
メソッド、または
synchronized
コード・ブロックを実行中のデーモン・スレッドがある場合。
synchronized
ブロックまたは
synchronized
メソッドが終了するや否や、デーモン・スレッドは突然に終了することになります。
stop()
、
suspend()
、および
resume()
メソッドの復活
この提案は実際的な理由から不可能と思われますが、
stop()
の使用停止を解きたいと思います (
Thread
と
ThreadGroup
の両方において)。ただし、
stop()
を呼び出してもコードを壊さないようにセマンティクスを変更します。ご記憶でしょうが、
stop()
の問題点は、スレッドが終了するとロックを放棄してしまうということです。これにより、スレッドが作業中のオブジェクトが、不安定な
(部分的に変更されている)
状態のままにされる可能性があります。それにもかかわらず、停止したスレッドがオブジェクトに対するロックを解放するので、そうしたオブジェクトへのアクセスが可能になります。
この問題は、ロックを保持していない場合に限ってスレッドを即座に終了するよう、
stop()
の動作を再定義することによって、解決できます。スレッドがロックを保持している場合は、スレッドが最後のロックを解放してから、それを終了するようにしたいと思います。この動作は、例外のトスに似た機構を使ってインプリメントすることができるでしょう。停止されたスレッドは、フラグをセットします。このフラグは、同期のとられたブロックがすべて終了すると、即座にテストされることになります。フラグがセットされている場合、暗黙の例外がスローされますが、この例外はキャッチ不能であり、スレッドが終了するときに何ら出力を生成するものとはなりません。MicrosoftのNTオペレーティング・システムが、外部から暗黙指定される突然の停止をあまり上手に処理できないということに注意してください。(ダイナミック・リンク・ライブラリーに停止の通知を行わないため、システム全体に渡るリソースのリークが生じる可能性があります。)単に
run()
を戻すという、例外に似たアプローチをお勧めするのは、これが理由です。
この例外スタイルのアプローチに伴う実際的な問題は、すべての
synchronized
ブロックの末尾に、「停止」フラグをテストするためのコードを挿入しなければならないということです。この余分のコードが原因で、プログラムの速度は下がり、そのサイズはより大きくなってしまうことでしょう。思い浮かぶ別のアプローチは、
stop()
に「怠惰な」停止をインプリメントさせるというものです。これは、この次にスレッドが
wait()
または
yield()
を呼び出したら、スレッドを停止するというものです。
Thread
に
isStopped()
メソッドと
stopped()
メソッドを追加したいと思います (これは、
isInterrupted()
と
interrupted()
によく似た方法で機能しますが、「停止要求」状態を検出します)。この解決策は、最初のものほど普遍的ではありませんが、おそらく実行可能と思われますし、オーバーヘッドも伴わないはずです。
suspend()
メソッドと
resume()
メソッドを、そのままJavaプログラム言語に戻すべきです。これらのメソッドは有用ですし、わたしは自分を幼稚園児のように扱ってもらいたいとは思いません。単にこれらのメソッドが危険なものになり得る
(スレッドが中断されるとき、ロックを保持している可能性がある)
という理由で、これらのメソッドを除去するのは侮辱です。これらのメソッドを使用するかどうかは、わたし自身に判断させてください。Sunは、受け取り側のスレッドがロックを保持している場合に
suspend()
が呼び出されたら、それを常に実行時例外とすることができるでしょう。もっと良いのは、実際の中断を、スレッドがロックを解放するまで延期することです。
ブロッキング入出力を正しく機能させる
wait()
と
sleep()
に限らず、どのようなブロッキング・オペレーションでも中断できるようであるべきです。この問題については、Taming
Java Threads
の第2章のソケットに関する文脈の中で説明しました。しかし、現在のところ、ソケットに対するブロッキング入出力オペレーションを中断する唯一の方法はソケットを閉じることであり、ファイルに対するブロッキング入出力オペレーションを中断する方法はありません。たとえば、いったん読み取り要求を開始すると、スレッドは、自分が実際に何かを読み取るまでブロックされます。ファイル・ハンドルを閉じたとしても、読み取りオペレーションが終了するわけではありません。
その上、プログラムは、ブロッキング入出力オペレーションのタイムアウトができなければなりません。ブロッキング・オペレーションが生じ得るすべてのオブジェクト
(InputStreamオブジェクトなど) は、次のようなメソッドをサポートしなければなりません。
InputStream s = ...;
s.set_timeout( 1000 );
|
これは、Socketクラスの
setSoTimeout(time)
メソッドに相当します。同様に、ブロッキング呼び出しに引数としてタイムアウトを渡すことができなければなりません。
ThreadGroup
クラス
ThreadGroup
は、スレッドの状態を変更することのできる、
Thread
のすべてのメソッドをインプリメントしなければなりません。わたし自身は、特に
join()
をインプリメントしたいと思います。グループ内のすべてのスレッドが終了するのを待機できるようにするためです。
結論
以上でわたしのリストはおしまいです。本稿の表題に示したとおり、これは、もしも自分が王様だったらの話です
(ため息)。上記の変更の一部 (あるいはそれに相当するもの)
が、いつかは言語に取り込まれることを希望しています。わたしは、Java言語は本当に優れた言語だと考えていますが、Javaのスレッド化モデルは十分に考え抜かれたものとは到底言えず、それを残念にも感じています。しかし、Javaプログラム言語は進化を続けており、状況を改善するための道はあるのです。
参考文献
著者について  | |  | Allen Holubは、1979年からコンピューター業界で働いています。
また、いろいろな雑誌 (主だったところでは、Dr. Dobb's Journal、Programmers Journal、Byte、MSJ など) で広く出版されているほか、
オンライン・マガジンJavaWorld の "Java Toolbox" 欄と、
IBM developerWorks Components zone の "OO設計プロセス (OO-Design Process)" 欄の執筆も行っています。
また、
ITworld Programming Theory & Practice
ディスカッション・グループの調整役でもあります。
Allenは自分名義の本を8冊出版しており、
その中の最新の著書 (Taming Java Threads) では、
Javaスレッド化の落とし穴について述べています。
彼は、自分で覚えていないくらい長い間、
オブジェクト指向ソフトウェアの設計と構築に取り組んできました。
AllenはC++ プログラマーを8年間を務めた後、
1996年初めにC++ からJavaに乗り換えました。
今では、C++ は悪夢だったと感じており、
その記憶はありがたいことに徐々に消え去りつつあります。
1982年以来、プログラミング (初めはC、次にC++ とMFC、
現在はOO設計とJavaプログラミング) を、
自分のクラスとカリフォルニア大学のバークレー公開講座の両方で教えています。
Allenは、Javaテクノロジーとオブジェクト指向設計のトピックについて、
公開クラスと社内トレーニングの両方を提供しています。
また、
オブジェクト指向設計のコンサルティングと契約Javaプログラミングも行っています。
AllenのWebサイトはwww.holub.com です。 |
記事の評価
|