スレッドセキュリティの概念、スレッドセキュリティを実現するいくつかの方法


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

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