効率的なスレッドセキュリティクラスの作成

7055 ワード

言語レベルでロックオブジェクトとスレッド間の送信をサポートすることで、スレッドセキュリティクラスの作成が簡単になります.本明細書では、効率的なスレッドセキュリティクラスの開発がどのように効果的で直感的であるかを説明するために、簡単なプログラミング例を使用します.
Javaプログラミング言語は、マルチスレッドアプリケーションの作成に強力な言語サポートを提供します.しかし,有用で誤りのないマルチスレッドプログラムの作成は依然として困難である.本稿では、プログラマが効率的なスレッドセキュリティクラスを作成するために使用できるいくつかの方法について概説します.
コンカレント性は、解決すべき問題がある程度のコンカレント性を必要とする場合にのみ、プログラマがマルチスレッドアプリケーションから利益を得ることができます.たとえば、印刷キューアプリケーションが1台のプリンタと1台のクライアントのみをサポートしている場合は、マルチスレッドとして記述するべきではありません.一般的に、同時性を含む符号化問題は、通常、同時実行可能な動作と、同時実行不可能な動作とを含む.例えば、複数のクライアントおよび1つのプリンタにサービスを提供する印刷キューは、印刷の同時要求をサポートすることができるが、プリンタへの出力はシリアル形式でなければならない.マルチスレッド実装は、インタラクティブアプリケーションの応答時間を改善することもできます.
Synchronizedキーワードは、マルチスレッド・アプリケーションのほとんどの操作を並列に実行できますが、グローバル・フラグの更新や共有ファイルの処理など、並列に実行できない操作もあります.これらの場合、他のスレッドがこの操作を実行するスレッドが完了する前に同じメソッドにアクセスしないように、ロックを取得する必要があります.Javaプログラムでは、このロックはsynchronizedキーワードで提供されます.リスト1はその使い方を説明しています.

public class MaxScore { int max; public MaxScore() { max = 0; } public synchronized void currentScore(int s) { if(s> max) { max = s; } } public int max() { return max; } }

ここで、2つのスレッドはcurrentScore()メソッドを同時に呼び出すことはできません.1つのスレッドが動作する場合、別のスレッドがブロックされなければなりません.ただし、max()メソッドは同期メソッドではないため、ロックとは無関係にmax()メソッドで最大値に同時にアクセスできるスレッドは任意の数あります.
リスト2に示すように、MaxScoreクラスに別のメソッドを追加することの影響を考えてみましょう.
インベントリ2.別のメソッドを追加

public synchronized void reset() { max = 0; }

このメソッド(アクセスされる場合)はreset()メソッドの他の呼び出しをブロックするだけでなく、両方のメソッドが同じロックにアクセスするため、MaxScoreクラスの同じインスタンスのcurrentScore()メソッドもブロックします.2つのメソッドが互いにブロックされない必要がある場合は、プログラマは同期をより低いレベルで使用する必要があります.インベントリ3は、2つの同期方法が互いに独立する必要がある場合がある別のケースである.
インベントリ3.2つの独立した同期方法

import java.util.*; public class Jury { Vector members; Vector alternates; public Jury() { members = new Vector(12, 1); alternates = new Vector(12, 1); } public synchronized void addMember(String name) { members.add(name); } public synchronized void addAlt(String name) { alternates.add(name); } public synchronized Vector all() { Vector retval = new Vector(members); retval.addAll(alternates); return retval; } }

ここで、2つの異なるスレッドは、membersとalternatesをJuryオブジェクトに追加することができます.synchronizedキーワードは、方法、より一般的に、任意のコードブロックにも使用できることを覚えておいてください.インベントリ4の2つのセグメントコードは等価である.
明細書4.等価なコード

synchronized void f() { void f() { // synchronized(this) { } // } }

したがって、addMember()およびaddAlt()メソッドが互いにブロックされないことを保証するために、Juryクラスをリスト5に示すように書き換えることができる.
リスト5.書き換え後のJuryクラス

import java.util.*; public class Jury { Vector members; Vector alternates; public Jury() { members = new Vector(12, 1); alternates = new Vector(12, 1); } public void addMember(String name) { synchronized(members) { members.add(name); } } public void addAlt(String name) { synchronized(alternates) { alternates.add(name); } } public Vector all() { Vector retval; synchronized(members) { retval = new Vector(members); } synchronized(alternates) { retval.addAll(alternates); } return retval; } }

Juryオブジェクトの同期に意味がないため、all()メソッドも変更する必要があります.書き換えられたバージョンでは、addMember()、addAlt()およびall()メソッドは、membersおよびalternatesオブジェクトに関連するロックにのみアクセスするため、Juryオブジェクトをロックしても無駄です.またall()メソッドは、本来リスト6に示す形式で書くことができることに注意してください.
インベントリ6.membersとalternatesを同期のオブジェクトとして使用する

public Vector all() { synchronized(members) { synchronized(alternates) { Vector retval; retval = new Vector(members); retval.addAll(alternates); } } return retval; }

しかし,我々はmembersとalternatesのロックを必要とする前に取得していたので,この効率は高くなかった.リスト5の書き換え形式は、最短時間でロックを保持し、毎回1つのロックしか得られないため、好ましい例である.これにより、後でコードを追加すると発生する可能性のある潜在的なデッドロックの問題を完全に回避することができる.
同期メソッドの分解前に見たように、同期メソッドはオブジェクトのロックを取得します.このメソッドが異なるスレッドで頻繁に呼び出される場合、パラレル性に制限があり、効率に制限があるため、このメソッドはボトルネックになります.このように,一般的な原則として,同期法をできるだけ少なくすべきである.この原則はありますが、1つの方法では、オブジェクトをロックする必要があるいくつかのタスクを完了する必要があり、同時にかなり時間がかかる他のタスクを完了する必要がある場合があります.これらの場合、ダイナミックな[ロック解除-ロック解除-ロック解除](Lock-Release-Lock-Release)メソッドを使用できます.例えば、インベントリ7およびインベントリ8は、このように変換可能なコードを示す.

public synchonized void doWork() { unsafe1(); write_file(); unsafe2(); }

インベントリ8.書き換え後の効率の高いコード

public void doWork() { synchonized(this) { unsafe1(); } write_file(); synchonized(this) { unsafe2(); } }

インベントリ7およびインベントリ8は、第1および第3の方法では、オブジェクトがロックされ、write_がより時間を要すると仮定する.file()メソッドでは、オブジェクトがロックされる必要はありません.ご覧のように、このメソッドを書き換えた後、このオブジェクトのロックは、最初のメソッドが完了した後に解放され、3番目のメソッドが必要なときに再取得されます.これでwrite_file()メソッドが実行されると、このオブジェクトのロックを待つ他のメソッドは実行できます.同期法をこのハイブリッドコードに分解すると,性能が著しく改善される.ただし、このコードに論理エラーを導入しないように注意する必要があります.
ネストされたクラスの内部クラスはJavaプログラムで注目される概念を実現し、クラス全体を別のクラスにネストすることができます.ネストされたクラスは、そのクラスを含むメンバー変数として使用されます.定期的に呼び出される特定のメソッドにクラスが必要な場合は、ネストされたクラスを構築できます.このネストされたクラスの唯一のタスクは、定期的に呼び出すために必要なメソッドです.これにより、プログラムの他の部分への依存性が解消され、コードがさらにモジュール化される.インベントリ9は、内部クラスを用いたグラフィッククロックのベースである.
public class Clock {
	protected class Refresher extends Thread {
		int refreshTime;
		public Refresher(int x) {
			super("Refresher");
			refreshTime = x;
		}

		public void run() {
			while(true) {
				try {
					sleep(refreshTime);
				}
				catch(Exception e) {}
				repaint();
			}
		}
	}

	public Clock() {
		Refresher r = new Refresher(1000);
		r.start();
	}

	private void repaint() {
		//          
		//       
	}
}

インベントリ9のコード例は、repaint()メソッドを他のコードで呼び出すものではない.これにより、クロックを大きなユーザインタフェースに組み込むのは簡単です.
イベントドライバ処理アプリケーションがイベントまたは条件(内部および外部)を反映する必要がある場合、システムを設計する2つの方法があります.第1の方法(ポーリングと呼ばれる)では、システムは定期的にこの状態を決定し、これに基づいて反映する.この方法は(簡単ですが)効率的ではありません.いつ呼び出す必要があるか分からないからです.
第2の方法(イベント駆動処理と呼ばれる)は効率が高いが、実現も複雑である.イベント駆動処理の場合、ある特定のスレッドがいつ実行されるべきかを制御する送信メカニズムが必要である.Javaプログラムでは、wait()、notify()、notifyAll()メソッドを使用してスレッドに信号を送信できます.これらの方法では、必要な条件が満たされるまでスレッドをオブジェクト上でブロックし、実行を再開できます.この設計は、スレッドがブロックされたときに実行時間を消費せず、notify()メソッドが呼び出されたときに直ちに起動できるため、CPUの消費量を低減する.ポーリングに比べて、イベント駆動メソッドはより短い応答時間を提供します.
効率的なスレッドセキュリティクラスを作成するには、スレッドセキュリティクラスを記述する最も簡単な方法はsynchronizedで各メソッドを宣言することです.このスキームは、データの破損を解消しますが、マルチスレッドからの収益も排除します.これにより、synchronizedブロック内で実行時間が最小限であることを分析し、確認する必要があります.ファイル、ディレクトリ、ネットワークソケット、データベースなどの遅いリソースへのアクセス方法に特に注意する必要があります.これらの方法は、プログラムの効率を低下させる可能性があります.このようなリソースへのアクセスは、できるだけ個別のスレッドに配置し、synchronizedコードの外に置くことが望ましい.
処理するファイルの中心リポジトリとして設計されています.getWork()とfinishWork()を使用してWorkTableクラスとドッキングするスレッドのセットとともに動作します.この例では、helperスレッドとハイブリッド同期を使用した全機能のスレッドセキュリティクラスを体験します.処理する新しいファイルを追加し続けるRefresher helperスレッドの使い方に注意してください.この例では、パフォーマンスを改善するために、refresherスレッドをwait()/notify()メソッドイベント駆動に変更したり、populateTable()メソッドを書き換えたりして、ディスク上のファイルのリスト(高コストの操作)に及ぼす影響を低減できる点は明らかです.
利用可能なすべての言語サポートを使用することで、Javaプログラムのマルチスレッドプログラミングはかなり簡単です.しかし、スレッドセキュリティクラスをより効率的にすることは依然として困難である.パフォーマンスを向上させるには、ロック機能を事前に考慮し、慎重に使用する必要があります.