目次


もしも自分が王様だったら: Java プログラム言語のスレッド化不具合への解決策提案

Comments

Java のスレッド化モデルは、この言語の最も満足のいかない点の 1つです。スレッド化が組み込まれているのは良いことですが、構文レベルおよびパッケージ・レベルでのスレッドのサポートが最小限に過ぎないので、非常に小さなアプリケーションでもない限り、何の役にも立ちません。

Java のスレッド化について論じた書籍の大部分は、Java のスレッド化モデルの不都合な点を並べ立て、そうした問題の一時しのぎとなるクラス・ライブラリーを紹介しています。そうしたクラスを「一時しのぎ」と呼ぶのは、そうしたクラスが解決する問題は、本来 Java 言語の構文の一部であるべきだからです。ライブラリーの代わりに構文的な手法を用いるなら、長い目で見て、より良いコードが生み出されることになります。なぜなら、コンパイラーと JVM が協働して、ライブラリーによるアプローチでは困難あるいは不可能な最適化を実行できるからです。

Taming Java Threads と題するわたしの著作 (参考文献参照)、およびこの記事では、さらに一歩進んで、スレッド化の問題に関するより積極的なアプローチを提案します。それは、Java プログラム言語に対し、本当の解決策となるいくつかの変更を加えるというものです。この記事とわたしの著作との間の主な相違は、この問題について考察を進めた結果、提案がもう少し改善されたという点にあります。これらの提案は暫定的なものに過ぎません。一人物による問題の考察に過ぎず、これを実行に移すには、多くの作業と専門家の意見を必要とすることでしょう。しかし、これははじまりとなるものです。わたしは、この問題に関するワーキング・グループを設立するつもりでいます。

ここに示す提案は、いくぶん大胆なものでもあります。これまで、何人かが、現在あいまいなものとなっている 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)xyz オブジェクトすべについて、ロックを獲得します。
synchronized(x || y || z)xyz オブジェクトのいずれかについて、ロックを獲得します。
synchronized( (x && y ) || z)先のコードに加えた、一目瞭然な拡張。
synchronized(...)[1000]単一のロックを獲得します。タイムアウトは 1秒です。
synchronized[1000] f(){...}f() に入ると this のロックを獲得します。ただし、タイムアウトは 1秒です。

TimeoutExceptionRuntimeException から派生したもので、待機がタイムアウトになるとスローされます。

タイムアウトは必要なものですが、コードを堅固なものにするには十分ではありません。ロックの獲得の待機を、外部から終了できるようにすることも必要です。したがって、ロックの獲得を待機しているスレッドに 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)).wait();

ここで、ab、および 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 クラスに対して以下の追加を行うことを提案します。

  1. wait_for_start() メソッドを追加する。このメソッドは、スレッドの run() が開始するまでブロックされます。(run が呼び出される前に待機中のスレッドが解放されれば、問題ありません。)この方法により、1つのスレッドが 1つ以上の補助スレッドを作成できるようになります。また、スレッドの作成から何かのオペレーションに移る前に、確実に補助スレッドが実行されているようにすることができるようになります。
  2. $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 プログラム言語のアクセス権の概念を、次のように引き締めたいと思います。

  1. パッケージ・アクセスを指定するのに、package キーワードを明示的に使用することを必須にする。わたしの考えでは、どのコンピューター言語であれ、デフォルトの動作があること自体が欠陥ですし、デフォルトのアクセス権さえ存在するということには当惑させられます (デフォルトが「非公開」ではなく「パッケージ」であることには、なおさら当惑させられます)。Java プログラム言語は、他の部分ではデフォルトのキーワードに相当するものを備えていません。明示的な package 指定子を必須にすると既存のコードを壊すことになるのは事実ですが、コードはずっと読みやすくなりますし、(たとえば、アクセス権の脱落が故意ではなく過失ということになるなら) あらゆる種類の潜在的なバグが取り除かれることになります。
  2. private protected を再び導入する。これは、現在の protected と同様に機能しますが、パッケージ・アクセスは許可しません。
  3. 外部のオブジェクトすべて (現在のオブジェクトと同じクラスのオブジェクトであっても) に対して private な「インプリメンテーション・アクセス」を指定するために、構文 private private を許可する。ドット (暗黙的であれ明示的であれ) の左に指定することが許可される唯一の参照は、this です。
  4. 特定のクラスにアクセスを許可できるよう、public の構文を拡張する。たとえば、以下のコードは、クラス Fred に属するオブジェクトに、some_method() を呼び出すことを許可します。しかし、メソッドは、その他のオブジェクト・クラスに対しては非公開となります。
  5. public(Fred) void some_method()
    {
    }

    この提案は、C++ の「フレンド」機構とは異なります。フレンド機構は、別のクラスのすべての非公開パーツに、クラスの全アクセスを許可するものです。ここでわたしが提案しているのは、限定されたメソッドのセットに対するアクセスを厳重に制御するということです。この方法により、あるクラスが、システムの残りの部分からは不可視である別のクラスとのインターフェースを定義することが可能になります。次のような、一目で理解できるバリアントもあります。

    public(Fred, Wilma) void some_method()
    {
    }

  6. 真に不変のオブジェクトを参照しているのでない限り、あるいは static final 基本データ型を定義しているのでない限り、すべてのフィールド定義を private とすることを必須とする。クラスのフィールドに直接アクセスできるということは、OO 設計の基本的な 2つの原則、すなわち抽象化とカプセル化に違反するものです。スレッド化の観点から言うと、フィールドへの直接アクセスを許可するなら、フィールドへの非同期アクセスを不注意に行う可能性が高まるに過ぎません。

  7. $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 がアプリケーションをシャットダウンしないという規則を作ることによって、この問題を解決したいと思います。

  1. 実行中の非デーモン・スレッドがある場合。
  2. synchronized メソッド、または synchronized コード・ブロックを実行中のデーモン・スレッドがある場合。

synchronized ブロックまたは synchronized メソッドが終了するや否や、デーモン・スレッドは突然に終了することになります。

stop()suspend()、および resume() メソッドの復活

この提案は実際的な理由から不可能と思われますが、stop() の使用停止を解きたいと思います (ThreadThreadGroup の両方において)。ただし、stop() を呼び出してもコードを壊さないようにセマンティクスを変更します。ご記憶でしょうが、stop() の問題点は、スレッドが終了するとロックを放棄してしまうということです。これにより、スレッドが作業中のオブジェクトが、不安定な (部分的に変更されている) 状態のままにされる可能性があります。それにもかかわらず、停止したスレッドがオブジェクトに対するロックを解放するので、そうしたオブジェクトへのアクセスが可能になります。

この問題は、ロックを保持していない場合に限ってスレッドを即座に終了するよう、stop() の動作を再定義することによって、解決できます。スレッドがロックを保持している場合は、スレッドが最後のロックを解放してから、それを終了するようにしたいと思います。この動作は、例外のトスに似た機構を使ってインプリメントすることができるでしょう。停止されたスレッドは、フラグをセットします。このフラグは、同期のとられたブロックがすべて終了すると、即座にテストされることになります。フラグがセットされている場合、暗黙の例外がスローされますが、この例外はキャッチ不能であり、スレッドが終了するときに何ら出力を生成するものとはなりません。Microsoft の NT オペレーティング・システムが、外部から暗黙指定される突然の停止をあまり上手に処理できないということに注意してください。(ダイナミック・リンク・ライブラリーに停止の通知を行わないため、システム全体に渡るリソースのリークが生じる可能性があります。) 単に run() を戻すという、例外に似たアプローチをお勧めするのは、これが理由です。

この例外スタイルのアプローチに伴う実際的な問題は、すべての synchronized ブロックの末尾に、「停止」フラグをテストするためのコードを挿入しなければならないということです。この余分のコードが原因で、プログラムの速度は下がり、そのサイズはより大きくなってしまうことでしょう。思い浮かぶ別のアプローチは、stop() に「怠惰な」停止をインプリメントさせるというものです。これは、この次にスレッドが wait() または yield() を呼び出したら、スレッドを停止するというものです。ThreadisStopped() メソッドと 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 のTaming Java Threads からの引用を更新したものです。本書は、Java プログラムのマルチスレッド化プログラムの落とし穴について詳細に説明しており、それらの問題を解決する、スレッド化に関係するクラスの Java パッケージも紹介しています。
  • Bill Pugh (University of Maryland) は、JLS のスレッド化モデルの修正のために、先頭に立って尽力しています。Bill の提案は、本稿に示したものほど広範囲に及ぶものではなく、主に既存のスレッド化モデルを妥当な方法で機能させることに焦点を当てています。The Java Memory Model (Java メモリー・モデル) Web サイトで、詳細な情報を入手できます。
  • 完全な Java Language Specification は、Sun の Web サイトでご覧になることができます。
  • スレッド化について高度に技術的なアプローチを行う場合は、Concurrent Programming in Java: Design Principles and Patterns, 2nd edition (Doug Lea 著) をご覧ください。これは優れた資料ですが、難解でアカデミックなスタイルで書かれており、近付きにくいと感じる読者もおられるかもしれません。Taming Java Threads の補足として良い資料です。
  • Java Threads (Scott Oaks、Henry Wong 共著) は、Taming Java Threads に比べて手軽な本ですが、スレッドのプログラミングの経験がない方にはより適しています。Oaks と Wong は Holub と同じヘルパー・クラスをいくつかインプリメントしています。類似の問題についての代替手段を調べるのは、いつでも有益なことです。
  • Threads Primer: A Guide to Multithreaded Programming (Bill Lewis、Daniel J. Berg 共著) は、スレッドへの優れた入門書です (Java 限定ではありません)。
  • Sun の Web サイトには、Java のスレッド化に関する技術情報が掲載されています。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=218801
ArticleTitle=もしも自分が王様だったら: Java プログラム言語のスレッド化不具合への解決策提案
publish-date=10012000