Javaスレッド(二):スレッド同期synchronizedとvolatile


前編では簡単な例でスレッドのセキュリティと非セキュリティについて説明したが、例ではセキュリティがない場合に出力される結果はちょうど1つずつ増加している(実は偶然で、何度も実行すると、異なる出力結果が発生する)、なぜこのような結果が発生したのか、確立されたCountオブジェクトはスレッド共有であり、1つのスレッドがメンバー変数num値を変更したため、次のスレッドは、修正されたnumを偶然読み取ったので、出力が増加します.
スレッド同期の問題を説明するには、まずJavaスレッドの2つの特性、可視性、秩序性について説明します.複数のスレッド間では、データインタラクションを直接伝達することはできません.これらのインタラクションは、変数を共有することによってのみ実現できます.前述の博文の例では、複数のスレッドの間でCountクラスのオブジェクトが共有されています.このオブジェクトはメインメモリ(スタックメモリ)に作成され、各スレッドには独自のワークメモリ(スレッドスタック)があり、ワークメモリにはメインメモリCountオブジェクトのコピーが格納されています.スレッドがCountオブジェクトを操作すると、まずメインメモリからワークメモリにCountオブジェクトがコピーされます.そしてコードcountを実行する.increment()はnum値を変更し、最後にワークメモリCountでメインメモリCountをリフレッシュします.1つのオブジェクトが複数のメモリにコピーされている場合、1つのメモリが共有変数を変更した場合、他のスレッドにも変更された値が表示されるはずです.これは可視性です.複数のスレッドが実行される場合、CPUのスレッドに対するスケジューリングはランダムであり、現在のプログラムがどのステップに実行されるか分からないまま次のスレッドに切り替わった.最も古典的な例は銀行送金問題であり、1つの銀行口座預金100であり、この場合、1人がその口座から10元を引き出し、同時にもう1人がその口座に10元送金すれば、残高は100であるべきである.この場合、Aスレッドが引き出しを担当し、Bスレッドが送金を担当し、Aがメインメモリから100、Bがメインメモリから100、Aがマイナス10操作を実行し、データをメインメモリにリフレッシュすると、メインメモリデータ100-10=90、Bメモリがプラス10操作を実行し、データをメインメモリにリフレッシュし、最後にメインメモリデータ100+10=110、明らかにこれは深刻な問題で、私たちはAスレッドとBスレッドが秩序正しく実行されることを保証し、先に金を引き出した後に送金したり、先に送金した後に金を引き出したりしなければならない.これは秩序性である.本稿ではJDK 5について述べる.0以前の従来のスレッドの同期方式、より高度な同期方式はJavaスレッド(8):ロックオブジェクトLock-同期問題のより完璧な処理方式を参照することができる.
スレッド同期の問題もコードで示します.TraditionalThreadSynchronized.JAva:2つのスレッドを作成し、同じオブジェクトの出力方法を実行します.
public class TraditionalThreadSynchronized {
    public static void main(String[] args) {
        final Outputter output = new Outputter();
        new Thread() {
            public void run() {
                output.output("zhangsan");
            }
        }.start();      
        new Thread() {
            public void run() {
                output.output("lisi");
            }
        }.start();
    }
}
class Outputter {
    public void output(String name) {
        // TODO      name           ,      name     
        for(int i = 0; i < name.length(); i++) {
            System.out.print(name.charAt(i));
            // Thread.sleep(10);
        }
    }
}

実行結果:
zhlainsigsan

明らかに出力する文字列は乱されて、私達の期待する出力の結果はzhangsanlisiで、これはスレッドの同期の問題で、私達はoutput方法が1つのスレッドの完全な実行に終わった後に更に次のスレッドに切り替えることを望んで、Javaの中でsynchronizedを使って1段のコードがマルチスレッドの実行の時に反発することを保証して、2つの使い方があります:1.synchronizedを使用して、反発が必要なコードを含め、ロックをかけます.
{
    synchronized (this) {
        for(int i = 0; i < name.length(); i++) {
            System.out.print(name.charAt(i));
        }
    }
}

このロックは、反発が必要な複数のスレッド間の共有オブジェクトでなければなりません.次のコードのように意味がありません.
{
    Object lock = new Object();
    synchronized (lock) {
        for(int i = 0; i < name.length(); i++) {
            System.out.print(name.charAt(i));
        }
    }
}

outputメソッドに入るたびに新しいlockが作成されます.このロックは明らかに各スレッドが作成され、意味がありません.2.synchronizedを反発が必要な方法に追加します.
public synchronized void output(String name) {
    // TODO       
    for(int i = 0; i < name.length(); i++) {
        System.out.print(name.charAt(i));
    }
}

この方式はthisでメソッド全体をロックするコードブロックに相当し,synchronizedで静的メソッドに加算すると,××××.classはメソッド全体のコードブロックをロックします.synchronizedを使用すると、場合によってはデッドロックになります.デッドロックの問題は後で説明します.synchronized修飾を用いた方法またはコードブロックは、原子操作と見なすことができる.
各ロックオブジェクト(JLSではmonitor)には2つのキューがあり、1つは準備キューであり、1つはブロックキューであり、準備キューはロックを取得するスレッドを格納し、ブロックキューはブロックされたスレッドを格納し、1つのスレッドが起動されると、準備キューに入り、CPUのスケジューリングを待つ.逆に、1つのスレッドがwaitされると、ブロックキューに入り、次の起動を待つと、スレッド間の通信に関連し、次のブログで説明します.我々の例を見ると、第1のスレッドが出力メソッドを実行すると、同期ロックが得られ、出力メソッドが実行され、ちょうどこのとき第2のスレッドも出力メソッドを実行するが、同期ロックが解放されていないことが判明し、第2のスレッドは準備キューに入り、ロックが解放されるのを待つ.1つのスレッドが反発コードを実行する手順は、次のとおりです.
    1.      ;

    2.       ;

    3.                ;

    4.     (       );

    5.        ;

    6.      。

したがってsynchronizedは、マルチスレッドの同時秩序性を保証するとともに、マルチスレッドのメモリ可視性を保証する.
volatileは2番目のJavaマルチスレッド同期のメカニズムで、JLS(Java LanguageSpecifications)によると、1つの変数はvolatileで修飾することができ、この場合、メモリモデル(メインメモリとスレッドワークメモリ)はすべてのスレッドが一致する変数値を見ることができることを確保し、コードを見てみましょう.
class Test {
    static int i = 0, j = 0;
    static void one() {
        i++;
        j++;
    }
    static void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}

一部のスレッドはoneメソッドを実行し、他のスレッドはtwoメソッドを実行し、twoメソッドはjがiより大きい値を印刷する可能性があり、前に分析したスレッド実行プロセスに従って分析します.
    1.    i           ;
    2.   i  ;
    3.        ;
    4.    j           ;
    5.   j  ;
    6.        ;

このときtwoメソッドを実行するスレッドは、プライマリメモリiの元の値を先に読み出し、jが変更された値を読み出し、プログラムの出力が予想された結果ではないことを招き、このような不合理な動作を阻止する1つの方法はoneメソッドとtwoメソッドの前にsynchronized修飾子を加えることである.
class Test {
    static int i = 0, j = 0;
    static synchronized void one() {
        i++;
        j++;
    }
    static synchronized void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}

前述の解析から,oneメソッドとtwoメソッドは二度と同時実行されず,iとjの値はメインメモリで一貫して保持され,twoメソッドの出力も一貫していることが分かった.もう1つの同期のメカニズムは、変数を共有する前にvolatileを追加することです.

class Test {
    static volatile int i = 0, j = 0;
    static void one() {
        i++;
        j++;
    }
    static void two() {
        System.out.println("i=" + i + " j=" + j);
    }
}

oneメソッドとtwoメソッドは同時に実行されるが、volatileを加えると共有変数iとjの変更をプライマリメモリに直接応答することができ、プライマリメモリにおけるiとjの値の一致性が保証されるが、twoメソッドを実行する際にtwoメソッドがiの値を取得し、jの値を取得する間、oneメソッドは何度も実行される可能性がある.jの値がiの値より大きくなる.したがってvolatileはメモリの可視性を保証し、同時秩序性を保証することはできません.
JLSでなぜ2つの変数を用いてvolatileの動作原理を述べるのか分からないが,これはよく理解できない.volatileは弱い同期手段であり、synchronizedに比べて、場合によってはブロックではないため、特に読み取り操作の場合、加算しないと影響がないように見え、書き込み操作を処理する場合、より多くの性能が消費される可能性があります.しかしvolatileとsynchronizedの性能の比較は、私もあまり正確ではありません.マルチスレッド自体は比較的玄なもので、CPUの時間スライスのスケジューリングに依存しています.JVMはもっと玄で、仮想マシンを研究したことがありません.最上階から下層に見ると、よく見えにくいです.JDK 5.0までにvolatileの使用シーンをクリアしていない場合は、使用しないで、できるだけsynchronizedで同期問題を処理し、スレッドブロックは簡単で乱暴です.またvolatileとfinalはフィールドを同時に修飾することはできませんが、なぜか考えてみてください.
高爽|Coder,原文アドレス:http://blog.csdn.net/ghsau/article/details/7424694、転載は明記してください.