JAVA同時設計モード学習ノート(一)——JAVAマルチスレッドプログラミング


このテーマは主に同時プログラミングの問題を討論して、すべての討論はJAVA言語の(その独特なメモリモデルと原生のマルチスレッドに対する支持能力のため)に基づいて、しかし本文は1種の分析の構想を伝えて、いかなる経験のある友达はすべて簡単にそれをいかなる言語に拡張することができます.
注:本文の主な参考資料は結城浩著『JAVAマルチスレッド設計モード』である.
スレッドの英語名Threadは、もともと「細い糸」を意味する.マルチスレッドプログラムでは,各スレッドの軌跡を追跡すると,一連の複雑な乱線団が派生する.実行中に「コードのどの部分まで実行しましたか?」と聞かれた場合、正しい場所を指摘するには、複数の指が必要です.
アプリケーションの規模、複雑さがある程度に達すると、コンカレント設計は必ず考慮される問題です.以下は一般的なアプリケーションです.
  • GUI:wordを例に、私たちは大規模なドキュメントを編集しています.このとき、「検索」操作を実行します.ワードが検索を行うと、同時に「検索を停止」ボタンが表示され、ユーザーはいつでも停止することができます.これでマルチスレッドが使用され、1つのスレッドがバックグラウンドで検索され、もう1つのスレッドが「検索を停止」ボタンを表示し、押すとすぐに検索を停止します.2つの操作は異なるスレッドで処理され,各スレッドは自分の機能に専念できるため,モジュール化設計思想の体現でもある.
  • 比較的時間のかかるI/O処理:ディスク、ネットワークのIO操作にかかる時間がメモリ処理よりはるかに大きいため、この時間内にプログラムが待機するだけで他の処理を実行できない場合、性能が大幅に低下する.I/O処理と非I/O処理を事前に区別できれば、I/O処理を行う際のCPUの空きギャップを利用して、他の処理を行うことができる.
  • 1 1つのサーバと複数のClient:ほとんどのサーバでは、1つ以上のClientを同時にサービスできることが要求されています.サーバ自体はいつClientアクセスがあるか分からないし、サーバに複数のClientの設計を直接導入するのは優雅な案ではない.したがって,クライアントがサーバに接続されると,自動的にこのクライアントを迎えるスレッドが生成されるように設計しておくとよい.これにより,サーバ側のプログラムは1つのClientだけにサービスを提供するように簡単に設計できる.もちろん、J 2 SE 1.4からは新しいNIOクラスライブラリが追加されており、スレッドを使わずに性能と拡張性を兼ね備えたI/O処理が可能であり、詳細はJDKを参照.
  • JAVAにおけるスレッドの符号化方式については,抽象クラスThreadから継承したりRunnableインタフェースを実現したりすることにほかならないが,読者の皆さんはよく知っていると思いますが,ここでは復唱しません.
    マルチスレッドプログラムでは、複数のスレッドが自由に操作できる以上、同じインスタンスに同時に操作できるのは当然です.この状況はまた不必要な面倒をもたらす.たとえば、古典的な銀行引き出しの問題では、「使用可能残高の確認」という部分のコードは次のようになります.
    if(           )
    {
                   
    }
    

    この論理自体は問題ない.利用可能残高を確認してから、入力金額の抽出が許可されているかどうかを確認し、システムが受け取ることができると決定した場合、利用可能残高からこの金額を差し引いて、利用可能残高がマイナスになることがないことを保証します.
    ただし,同時に2つ以上のスレッドがこのプログラムのコードを実行すると,利用可能残高が負になる可能性がある.例:
       ……
         = 1000 
          = 1000 
      A——           ?
      A—— 
    <<<       B>>>
      B——           ?
      B—— 
      B——             
      B——      0 
    <<<       A>>>
      A——             
      A——      -1000 

    時間差によりスレッドBがスレッドAの「利用可能残高の確認」と「利用可能残高の減算」の間に挟まれる場合があり,金額が負の場合が生じることが分かった.
    JAVAではsynchronizedを用いて共有反発を実現しており、交差点の信号処理のようなものである.まっすぐ走行すると青信号になり、反対側の横のランプは赤信号に違いない.synchronizedも同様の「交通規制」方式でスレッド間の反発を実現している.
    上記の銀行預金の反発は以下のように実現される.
    public class Bank
    {
        private int money;
        private String name;
    
        public Bank(String name, int money)
        {
            this.money = money;
            this.name = name;
        }
    
        //   
        public synchronized void deposit(int m)
        {
            money += m;
        }
    
        //   
        public synchronized void withdraw(int m)
        {
            if (money >= m)
            {
                money -= m;
                return true;  //    
            }
            else
            {
                return false; //     
            }
        }
    
        public String getName()
        {
            return name;
        }
    }

    1つのスレッドがBankインスタンスのdepositメソッドまたはwithdrawメソッドを実行している場合、他のスレッドは同じインスタンスのdepositメソッドおよびwithdrawメソッドを実行できません.実行するスレッドは、キューに並んで待たなければなりません.
    Bankクラスにはsynchronized以外の方法であるgetNameがあることに気づくかもしれません.他のスレッドが同じインスタンスのdeposit、withdraw、getNameメソッドを実行しているかどうかにかかわらず、その実行を妨げません.
    synchronizedバリアのいくつかの使用方法は、以下の通りです.
    synchronizedローカルバリア:「規制」が必要なのはメソッド全体ではなく、メソッドの一部である場合、このようなバリアを使用します.コードは次のとおりです.
    synchronized(   )
    {
        ……
    }

    synchronizedインスタンスメソッドブロック:インスタンスメソッド全体でなくメソッドの一部を「規制」する必要がある場合は、このようなブロックを使用します.コードは次のとおりです.
    synchronized void method()
    {
        ……
    }

    このコードは機能的に以下のコードと異曲同工の妙がある.
    void method()
    {
        synchronized(this)    
        {
            ……
        }
    }

    synchronizedクラスメソッドバリア:「規制」が必要な場合は、次のようなバリアを使用します.
    
    class Something
    {
        static synchronized void method()
        {
            ……
        }
    }

    このコードは機能的に以下のコードと異曲同工の妙がある.
    
    class Something
    {
        static void method()
        {
            synchronized(Something.class)
            {
                ……
            }
        }
    }

    以上から,synchronizedクラスは本質的にクラスオブジェクトをロックされたオブジェクトとして反発することを阻止する.
    線面講義スレッドの調整.上で述べたのは最も簡単な共有反発であり、スレッドがあれば実行しておとなしく待つ.現実の仕事では、私たちが必要とするのはそれだけではありません.例えば、
  • 空きがあれば書き続け、なければ待つ.
  • スペースに空きがある場合は、待機スレッドをタイムリーに通知します.
  • スレッド調整の具体的な実装については、次の章で説明します.
    JAVAでは、このような条件処理をサポートするためにwait、notify、notifyAllが提供されています.以下の点に注意してください.
  • あるインスタンスのwaitメソッドを実行するには、まず、そのインスタンスのロックを取得しなければならない.wait set(スレッドの休憩室)に入ると、自動的にロックが解除されます.
  • notifyメソッドを使用すると、ロックインスタンスのwait setからスレッドが呼び出されます.同様に、notyfyメソッドを呼び出すには、スレッドが最初にロックされる必要があります.起動されたスレッドは、notifyのスレッドがロックを占有しているため、すぐに実行できるわけではありません.また、notifyを実行する場合、wait setには複数のスレッドが待機していると仮定し、そのスレッドを具体的に選択することはわかりませんので、アプリケーションは選択したスレッドによって変動するような方法で書かないほうがいいです.
  • notifyAllはnotifyとほぼ同じで、唯一の違いは、すべてのスレッドを1つではなく起動することです.一般的にnotifyAllで書かれたプログラムはもっと丈夫です.