基本情報技術者 2010年 秋期 午後 問11
次のJavaプログラムの説明及びプログラムを読んで、設問1,2に答えよ。
〔プログラムの説明〕
電子会議システムのプログラムである。
電子会議は、サーバに接続しているクライアント間で行われ、一つのクライアントは、会議の参加者1人に対応する。会議に参加するときは、電子会議システムのサーバにログインする。参加者の発言はクライアントからのメッセージとしてサーバに送られる。サーバは受信したメッセージを、発信元クライアントを含む全クライアントに配信する。会議から退出するときは、ログアウトする。
この電子会議システムのサーバを実装するために、次のクラスを定義する。
(1) クラスMessageQueueは、クライアントからのメッセージを格納するための待ち行列(メッセージキュー)である。メッセージは、先入れ先出しで管理される。メソッドputは、引数messageで与えられたメッセージをメッセージキューに追加する。メッセージキューに格納できるメッセージ数には制限があり、満杯のときは、空きができるまで待つ。メソッドtakeは、メッセージキューの先頭からメッセージを取り出す。メッセージキューが空のときは、メッセージが追加されるまで待つ。
(2) クラスConfServerは、サーバを定義する。クライアントとのログイン状態を管理し、メッセージの受信と配信を行う。ここでクライアントとは、(3)で説明する抽象クラスの型のインスタンスである。クラスConfServerは、クラスが初期化されるときにインスタンスが作成され、単独のスレッドとして動作する。メソッドrunは、メッセージキューからメッセージを取り出し、ログインしている全クライアントにそのメッセージを配信する。この操作を繰り返す。メソッドloginは、入れ子クラスConfServer.Sessionのインスタンスを生成し、それをキーとしてクライアントを管理テーブルに登録し、そのインスタンスを返す。クライアントが既に登録されている場合は、IllegalArgumentExceptionを投げる。クライアントが管理テーブルに登録されているとき、そのクライアントはサーバにログインしている状態であるとする。
(3) 抽象クラスConfClientは、サーバが必要とするクライアントの機能を定義する。サーバは、メソッドdisplayMessageを呼び出してクライアントにメッセージを配信する。クライアントは、このクラスを実装しなければならない。クライアントがサーバにメッセージを送信するときは、ログイン時に返されたクラスConfServer.SessionのインスタンスのメソッドwriteMessageを呼び出す。クライアントがログアウトするときは、同じインスタンスのメソッドlogoutを呼び出す。
(4) サーバをテストするために、クラスTestClientを定義する。このクラスは、ConfClientで定義されたメソッドをテスト用に実装する。TestClientのインスタンスは、サーバに対してクライアントの役割をする。メソッドdisplayMessageは、メッセージを次の形式で出力する。
発信クライアント名: メッセージ >受信クライアント名




設問1:
問題文を見る プログラム中のに入れる正しい答えを、解答群の中から選べ。
aに関する解答群
ア:!queue.isEmpty()
イ:queue.isEmpty()
ウ:queue.size() != MAX_SIZE
エ:queue.size() < MAX_SIZE
オ:queue.size() == MAX_SIZE
bに関する解答群
ア:session.get(message)
イ:session.get(server)
ウ:session.get(sessionsTable)
エ:sessionsTable.get(message)
オ:sessionsTable.get(server)
カ:sessionsTable.get(session)
cに関する解答群
ア:queue
イ:server
ウ:session
エ:sessionsTable
オ:this
dに関する解答群
ア:abstract
イ:extends
ウ:implements
エ:static
オ:throws
eに関する解答群
ア:super()
イ:super(name)
ウ:super(this)
エ:this()
オ:this(name)
模範解答
a:イ、b:カ、c:オ、d:イ、e:イ
解説
解答の導き方
まず全体の流れを確認します。クライアントが送ったメッセージは、メッセージキュー(届いたメッセージを順番に一時保管する場所)に入ります。ConfServerのスレッド(プログラム内で処理を進める流れ)がキューからメッセージを取り出し、ログイン中のクライアントに配信します。
a:キューが空のときに待つ条件
問題文には、メッセージキューが空なら、メッセージが追加されるまで待つとあります。したがって、takeの中では、キューが空の間、wait()で待つ必要があります。
キューが空であることを表すのはqueue.isEmpty() です。よって、aにはqueue.isEmpty() が入ります。メッセージを取り出した後はnotifyAll() を呼び、満杯のキューへの追加を待っている処理を起こします。
putでは、満杯のときに待ちます。一方、takeでは、空のときに待ちます。待つ条件を取り違えないことが大切です。また、条件を確認する部分がwhileになっているので、起こされた後にも、キューが空かどうかをもう一度確認します。
b:配信先のクライアントを取り出す
sessionsTableの型はMap<Session, ConfClient> です。Mapは、キーと値を対応付けて管理する表です。この表では、Sessionがキー、ConfClientが値です。
deliverMessageでは、sessionsTable.keySet()からSessionを一つずつ取り出しています。そのSessionに対応するクライアントをsessionsTable.get(session)で取り出し、そのクライアントのdisplayMessageを呼び出せば、メッセージを配信できます。
したがって、bにはsessionsTable.get(session) が入ります。messageは配信するメッセージであり、sessionsTableのキーではありません。
c:ログアウトするセッション
Sessionのlogoutメソッドは、ログアウトするセッションをlogoutImplに渡します。logoutImplはSessionを受け取り、そのSessionをキーとしてsessionsTableから削除します。
このlogoutメソッドを呼び出している現在のSessionのインスタンスを表すのがthisです。そのため、cにはthisが入ります。serverはサーバのインスタンスを表すので、Sessionを渡す場所には使えません。
d:ConfClientを基にしたクラスを作る
問題文では、TestClientはConfClientで定義されたメソッドをテスト用に実装するとあります。ConfClientは抽象クラス(共通の機能を定義し、一部の処理を子クラスに実装させるクラス)です。
クラスを基に別のクラスを作るときは、extendsを使います。TestClientはConfClientを継承し、ConfClientが宣言しているdisplayMessageを実装するため、dにはextendsが入ります。implementsはインタフェースを実装するときに使うため、この場合は当てはまりません。
e:親クラスのコンストラクタを呼び出す
ConfClientのコンストラクタはString型のnameを受け取り、その値を名前として設定します。TestClientのコンストラクタも受け取ったnameを親クラスに渡す必要があります。
親クラスのコンストラクタを呼び出す書き方はsuper(name)です。したがって、eにはsuper(name)が入ります。super()は引数を渡さない呼び出しですが、ConfClientには引数なしのコンストラクタがありません。
誤りやすいポイント
- takeとputで、待つ条件を混同すること
putはキューが満杯のときに待ち、takeはキューが空のときに待ちます。 - Mapから取り出すキーと値を取り違えること
sessionsTableではSessionがキー、ConfClientが値です。配信先として必要なのはConfClientです。 - thisをサーバのインスタンスと考えること
thisは、そのメソッドを呼び出している現在のSessionのインスタンスを表します。 - extendsとimplementsを混同すること
クラスを継承するときはextendsを使います。implementsはインタフェースを実装するときに使います。 - super(name)とthis(name)を混同すること
super(name)は親クラスのコンストラクタを呼び出します。this(name)は同じクラスの別のコンストラクタを呼び出します。
FAQ
Q: キューが空かどうかの確認に、なぜwhileを使うのですか?
A: wait()から戻ったとき、必ずメッセージがあるとは限らないためです。条件をもう一度確認し、まだ空なら待ち続けます。
A: wait()から戻ったとき、必ずメッセージがあるとは限らないためです。条件をもう一度確認し、まだ空なら待ち続けます。
Q: sessionsTable.get(message)ではいけないのですか?
A: sessionsTableのキーはSessionです。messageは配信する文字列なので、キーとして対応付けられていません。
A: sessionsTableのキーはSessionです。messageは配信する文字列なので、キーとして対応付けられていません。
Q: TestClientでnameを直接設定せず、super(name)を使うのはなぜですか?
A: nameはConfClientのprivateなフィールドです。ConfClientのコンストラクタに値を渡すことで、親クラス側で名前を設定します。
A: nameはConfClientのprivateなフィールドです。ConfClientのコンストラクタに値を渡すことで、親クラス側で名前を設定します。
関連キーワード: メッセージキュー、先入れ先出し、同期処理、Map、クラス継承、コンストラクタ
設問2:
問題文を見る クラスMessageQueueのメソッドput及びtakeには、synchronized修飾子が付けられている。これには理由が二つある。一つは、キューの状態(空又は満杯)によってスレッド間でメソッドwait及びnotifyAllを呼び出して同期を取るためである。もう一つの理由として適切な答えを、解答群の中から選べ。
解答群
ア:クラスConfClientのインスタンスがそれぞれ別スレッドとして同時に動くことを想定すると、一度にフィールドqueueで参照されるLinkedListのインスタンスにアクセスが集中することが考えられる。そこで、一時点でqueueにアクセスできるスレッドを一つにして、システムに負荷がかかりすぎるのを防ぐ。
イ:クラスConfClientのインスタンスがそれぞれ別スレッドとして同時に動くことを想定すると、タイミングによってはフィールドqueueで参照されるLinkedListのインスタンスに格納されている他のスレッドが書き込んだメッセージを読み出すことができ、セキュリティ上問題となる。そこで、一時点でqueueにアクセスできるスレッドを一つにして、queueに既に書き込まれているメッセージを読み出せないようにする。
ウ:クラスConfClientのインスタンスとクラスConfServerのインスタンスは別スレッドとして動くので、フィールドqueueで参照されるLinkedListのインスタンスに対しての操作が同時に実行されることがある。そこで、一時点でqueueにアクセスできるスレッドを一つにして、データの内部状態に矛盾が起きないようにする。
エ:クラスConfClientのインスタンスの個数が多いとフィールドqueueで参照されるLinkedListのインスタンスに格納するメッセージ数の増大によってメモリ不足によるエラーが発生し、ConfServerを実行しているスレッドが停止する可能性がある。そこで、ConfClientのインスタンスの個数を制限して、エラーを起こさないようにする。
模範解答
ウ
解説
解答の導き方
この設問は、メソッドputとtakeにsynchronizedが付いている理由のうち、waitやnotifyAllを使うためという理由以外のものを問うています。
-
メッセージキューを操作するのは、異なるスレッドです。
スレッドは、プログラム内で処理を進める実行単位です。問題文では、クライアントから送られたメッセージをキューに追加し、サーバがキューから取り出すと説明されています。つまり、クライアント側の処理がputを呼び、サーバ側の処理がtakeを呼びます。これらが別々のスレッドで動くと、同時にキューを操作する可能性があります。 -
同じキューを同時に操作すると、データの状態が壊れるおそれがあります。
putはメッセージを追加し、takeは先頭のメッセージを取り出します。これらの操作が重なると、メッセージを追加・削除する処理の途中に、別の処理が割り込む可能性があります。LinkedListは、要素のつながりを内部で管理するデータ構造です。同時に変更されると、そのつながりが矛盾し、正しくメッセージを追加・取り出しできなくなるおそれがあります。 -
synchronizedは、同じMessageQueueのインスタンスに対する操作を一つずつ実行させます。
putとtakeの両方にsynchronizedが付いているため、同じインスタンスでは、どちらかのメソッドを実行している間、もう一方のメソッドは待ちます。これにより、複数のスレッドが同時にキューを変更するのを防ぎます。
したがって、内部状態に矛盾が起きないようにするというウが適切です。
選択肢のほかの理由も確認します。
- アは、目的をシステムの負荷を抑えることとしています。しかし、synchronizedを付ける主な理由は、アクセスの集中を避けることではありません。同時に操作してデータの状態が壊れるのを防ぐためです。
- イは、他のスレッドが書き込んだメッセージを読めないようにするとしています。しかし、このシステムでは、キューのメッセージをサーバが取り出して全クライアントに配信します。ほかのスレッドのメッセージを読むことは、もともとの動作に含まれています。
- エは、クライアント数を制限するためとしています。しかし、問題文ではキューに格納できるメッセージ数に制限があり、満杯なら空きができるまで待つと説明されています。クライアント数を制限する処理とは別の話です。
誤りやすいポイント
- synchronizedは、単に処理を遅くしたり、アクセスを減らしたりするためのものではありません。同じデータへの同時操作を避け、データの状態を守る役割があります。
- waitやnotifyAllを使うための同期と、同時にキューを操作させないための同期は、どちらもsynchronizedを付ける理由です。この設問では、後者が選択肢から問われています。
- 「別のスレッドが書いたメッセージを読める」こと自体は問題ではありません。メッセージを共有して配信することが、このプログラムの目的です。
FAQ
Q: synchronizedが付いていると、キューには一つのスレッドしかアクセスできないのですか?
A: 同じMessageQueueのインスタンスについて、putやtakeの処理を同時には実行できません。一方の処理が終わると、待っていた別の処理が進めるようになります。
A: 同じMessageQueueのインスタンスについて、putやtakeの処理を同時には実行できません。一方の処理が終わると、待っていた別の処理が進めるようになります。
Q: waitやnotifyAllと、データの状態を守ることは別の目的ですか?
A: はい。waitやnotifyAllは、キューが満杯または空のときに、スレッドを待たせたり、待ち状態を知らせたりするために使います。synchronizedには、これに加えて、複数のスレッドがキューを同時に操作しないようにする役割もあります。
A: はい。waitやnotifyAllは、キューが満杯または空のときに、スレッドを待たせたり、待ち状態を知らせたりするために使います。synchronizedには、これに加えて、複数のスレッドがキューを同時に操作しないようにする役割もあります。
関連キーワード: スレッド、排他制御、メッセージキュー、LinkedList、synchronized



