JavaマルチスレッドとJavaメモリモデルの基本を理解するための記事


前に書かれている:マルチスレッドについて言及したほとんどの学生は、マルチスレッドが何であるか、いつ使用できるか、使用中に共有変数の問題があるかどうかなどを知らずに眉をひそめるかもしれません。この記事は2つのパートに分かれており、最初のパートはマルチスレッドの基本を説明することであり、2番目のパートはJavaメモリモデルを説明することです。

ここに画像の説明を挿入

1.マルチスレッドライフサイクルと5つの基本状態

Javaマルチスレッドのライフサイクル。最初に次の古典的な図を見てください。基本的に、Javaのマルチスレッドに関する重要な知識が含まれています。
ここに画像の説明を挿入
Javaスレッドには5つの基本状態があります

  • 新しい状態(新規):スレッドオブジェクトのペアが作成されると、次のような新しい状態になります。Thread t = new MyThread();

  • 準備完了状態(実行可能):スレッドオブジェクトのstart()メソッドが呼び出されると(t.start();)、スレッドは準備完了状態になります。準備完了状態のスレッドは、CPUがいつでも実行をスケジュールするのを待機する準備ができていることを意味し、t.start()を実行した直後にスレッドが実行されることを意味しません。

  • 実行状態(実行中):CPUがスレッドを準備完了状態でスケジュールし始めると、この時点でスレッドを実際に実行できます。つまり、実行状態に入ります。注:実行可能状態への唯一の入り口は、作動可能状態です。つまり、スレッドが実行のために実行状態に入ろうとする場合、最初に作動可能状態でなければなりません。

  • Blocked(Blocked):実行状態のスレッドは、なんらかの理由でCPUを使用する権利を一時的に放棄して実行を停止します。このとき、スレッドは準備完了状態になるまでブロック状態に入り、その後再びCPUから呼び出されて入る機会があります。走行状態に。ブロッキングのさまざまな理由により、ブロッキングステータスは次の3つのタイプに分類できます。

1.ブロッキングを待機中:実行状態のスレッドはwait()メソッドを実行して、スレッドを待機ブロッキング状態にします。

2.同期ブロッキング-スレッドは同期ロックの取得に失敗します(ロックが他のスレッドによって占有されているため)、同期ブロッキング状態になります。

3.その他のブロッキング-スレッドのスリープ()または結合()を呼び出すか、I / O要求を発行すると、スレッドはブロッキング状態になります。sleep()状態がタイムアウトになるか、join()がスレッドの終了またはタイムアウトを待機するか、I / O処理が完了すると、スレッドは再び準備完了状態になります。

Dead(Dead):例外のために、スレッドは実行を終了するか、run()メソッドを終了し、スレッドはそのライフサイクルを終了します。

第二に、Javaマルチスレッドの作成と開始

Javaでのスレッド作成には3つの基本的な形式があります

1. Threadクラスを継承し、このクラスのrun()メソッドを書き換えます

Threadクラスを継承し、このクラスのrun()メソッドを書き換えます

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0 ;i < 50;i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
    public static void main(String[] args) {
        for (int i = 0;i<50;i++) {
            //调用Thread类的currentThread()方法获取当前线程
            System.out.println(Thread.currentThread().getName() + " " + i);
            if (i == 10) {
                new MyThread().start();
                new MyThread().start(); 
            }
        }
    }
}

演算結果:

...
main 48
main 49
Thread-00
Thread-01
Thread-02
Thread-03
Thread-04
Thread-10
...

これは、図からわかるように、コードを実行した後の結果です。

1. 3つのスレッドがあります:メイン、スレッド0、スレッド1

2. 2つのスレッドThread-0およびThread-1によって出力されるメンバー変数iの値は連続的ではありません(iはローカル変数ではなくインスタンス変数です)。スレッドクラスを継承してマルチスレッドを実装する場合、各スレッドの作成で異なるサブクラスオブジェクトを作成する必要があるため、2つのスレッドThread-0とThread-1はメンバー変数iを共有できません。

3.スレッドの実行はプリエンプティブであり、スレッド0またはスレッド1が常にCPUを占有しているとは言えません(これもスレッドの優先順位に関連しています。ここで、スレッド0とスレッド1のスレッドの優先順位は同じであり、スレッドの優先順位に関する知識ここを展開しないでください)

2. Runnableインターフェースを実装してスレッドクラスを作成します。

Runnableインターフェースを実装するクラスを定義し、このクラスのインスタンスオブジェクトobjを作成し、objをコンストラクターパラメーターとしてThreadクラスインスタンスオブジェクトに渡します。このオブジェクトは実際のスレッドオブジェクトです

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0 ;i < 50 ;i++) {
            System.out.println(Thread.currentThread().getName()+":" +i);
        }
    }

    public static void main(String[] args) {
       for (int i = 0;i < 50;i++) {
            System.out.println(Thread.currentThread().getName() + ":" +i);
            if (i == 10) {
                MyRunnable myRunnable = new MyRunnable();
                new Thread(myRunnable).start();
                new Thread(myRunnable).start();
            }
        }
        //java8 labdam方式
         new Thread(() -> {
            System.out.println(Thread.currentThread().getName());
        },"线程3").start();
    }
}

演算結果:

...
main:46
main:47
main:48
main:49
Thread-028
Thread-029
Thread-030
Thread-130
...

1.スレッド1とスレッド2によって出力されるメンバー変数iは連続的です。つまり、この方法でスレッドを作成すると、複数のスレッドがスレッドクラスのインスタンス変数を共有できるようになります。変数。ただし、上記のコードを使用して実行すると、結果が実際には連続的でないことがわかります。これは、複数のスレッドが同じリソースにアクセスするときに、リソースがロックされていないと、スレッドの安全性の問題が発生するためです(これはここでは拡張されていないスレッド同期の知識);
2、java8はラムダを使用して複数のスレッドを作成できます。

3. CallableおよびFutureインターフェースを介してスレッドを作成します

Callableインターフェース実装クラスを作成し、スレッド実行本体として機能するcall()メソッドを実装します。メソッドには戻り値があり、次にCallable実装クラスのインスタンスを作成します。FutureTaskクラスを使用してCallableオブジェクトをラップし、FutureTaskオブジェクトはCallableオブジェクトのcall()メソッドの戻り値。FutureTaskオブジェクトをThreadオブジェクトのターゲットとして使用して、新しいスレッドを作成および開始します。FutureTaskオブジェクトのget()メソッドを呼び出して、子スレッドの実行後に戻り値を取得します。

public class MyCallable implements Callable<Integer> {
    private int i = 0;
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (; i < 100; i++) {
            System.out.println(Thread.currentThread().getName() + " " + i);
            sum += i;
        }
        return sum;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建MyCallable对象
        Callable<Integer> myCallable = new MyCallable();
        //使用FutureTask来包装MyCallable对象
        FutureTask<Integer> ft = new FutureTask<Integer>(myCallable);
        for (int i = 0;i<50;i++) {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            if (i == 30) {
                Thread thread = new Thread(ft);
                thread.start();
            }
        }
        System.out.println("主线程for循环执行完毕..");
        Integer integer = ft.get();
        System.out.println("sum = "+ integer);
    }
}

call()メソッドの戻り値の型は、FutureTaskオブジェクトが作成されたときの<>の型と同じです。

3、Javaメモリモデルの概念

並行プログラミングでは、2つの重要な問題に対処する必要があります。スレッド間で通信する方法とスレッド間で同期する方法です(ここでのスレッドとは、同時に実行されるアクティブなエンティティを指します)。通信とは、スレッドが情報を交換するメカニズムのことです。命令型プログラミングでは、スレッド間に2つの通信メカニズムがあります。共有メモリとメッセージパッシングです。

共有メモリのコンカレントモデルでは、スレッドはプログラムの共通の状態を共有し、スレッドはメモリの共通の状態を読み書きすることによって暗黙的に通信します。メッセージパッシングの同時実行モデルでは、スレッド間に共通の状態はなく、スレッドはメッセージを明示的に送信することによって明示的に通信する必要があります。

ヒープメモリはスレッド間で共有されます(この記事では、「共有変数」という用語を使用して、インスタンスドメイン、静的ドメイン、および配列要素を指します)。ローカル変数、メソッド定義パラメーター(Java言語仕様では正式メソッドパラメーターと呼ばれます)、および例外ハンドラーパラメーターはスレッド間で共有されず、メモリの可視性の問題も発生しません。メモリモデルの影響を受けます。

メインメモリとワーキングメモリの解釈

メインメモリ:クラスのインスタンスが存在する領域すべてのインスタンスがメインメモリに存在し、インスタンスのフィールドもここにあります。メインメモリはすべてのスレッドで共有され、メインメモリは主にJavaヒープ内のオブジェクトのインスタンスデータ部分に対応します。

作業メモリー:各スレッドには独自の作業領域があります。作業メモリーには、作業コピーと呼ばれるメインメモリーの部分的なコピーがあります。

Javaスレッド間の通信は、Javaメモリモデル(この記事ではJMMと呼ばれます)によって制御されます。JMMは、スレッドによる共有変数の書き込みが別のスレッドにいつ表示されるかを決定します。抽象的観点から、JMMはスレッドとメインメモリ間の抽象的な関係を定義します。スレッド間の共有変数はメインメモリに格納され、各スレッドにはプライベートローカルメモリ(ローカルメモリ)があります。 、ローカルメモリは、共有変数のコピーを読み書きするためのスレッドを格納します。ローカルメモリはJMMの抽象的な概念であり、実際には存在しません。キャッシュ、書き込みバッファー、レジスター、およびその他のハードウェアとコンパイラーの最適化について説明します。Javaメモリモデルの抽象的な模式図は次のとおりです。

ここに画像の説明を挿入
上の図から、スレッドAとスレッドBの間で通信する場合は、次の2つの手順を実行する必要があります。

  1. 最初に、スレッドAはローカルメモリAで更新されたシェア変数をメインメモリに更新します。
  2. 次に、スレッドBはメインメモリに移動して、スレッドAが以前に更新した共有変数を読み取ります。

次の図は、概略図で2つのステップを
ここに画像の説明を挿入
示しています。上の図に示すように、ローカルメモリAとBには、メインメモリに共有変数xのコピーがあります。最初に、3つのメモリすべてのx値が0であるとします。
1.スレッドAの実行中、更新されたx値(値が1と仮定)は、ローカルメモリAに一時的に格納されます。スレッドAとスレッドBが通信する必要がある場合、スレッドAはまずローカルメモリ内の変更されたx値をメインメモリに更新します。このとき、メインメモリ内のx値は1になります。
2.スレッドBがメインメモリに移動して、スレッドAの更新されたx値を読み取ります。このとき、スレッドBのローカルメモリのx値も1になります。
全体として見ると、これらの2つのステップは基本的にスレッドAがメッセージをスレッドBに送信することであり、この通信プロセスはメインメモリを経由する必要があります。JMMは、メインメモリーと各スレッドのローカルメモリー間の相互作用を制御することにより、Javaプログラマーにメモリーの可視性を保証します。

第四に、メモリ間のインタラクティブな操作

メインメモリとワーキングメモリ間の相互作用は、8つのアトミック操作を定義します。詳細は次のとおりです。

  • ロック:メインメモリに作用し、変数をスレッドの排他状態として識別する変数

  • ロック解除(ロック解除):メインメモリに作用し、ロック状態の変数を解放する変数

  • 読み取り(読み取り):メインメモリに作用する変数。メインメモリからスレッドの作業メモリに変数の値を転送します。

  • load:ワーキングメモリに作用する変数。読み取りから転送された変数の値をワーキングメモリの変数のコピーに配置またはコピーします。

  • use(use):作業メモリーに作用する変数。つまり、スレッドは作業メモリー内の変数の値を参照し、作業メモリー内の変数の値を実行エンジンに渡します。

  • 割り当て(割り当て):スレッドが指定された値を作業メモリ内の変数に割り当てることを示す、作業メモリに作用する変数。

  • ストア(ストレージ):ワーキングメモリに作用し、ワーキングメモリ内の変数の値をメインメモリに転送する変数

  • 書き込み:メインメモリ内の変数に書き込み、ストアによって渡された変数値をメインメモリ内の対応する変数に書き込みます

下の写真は私たちの印象を深めるのに役立ちます
ここに画像の説明を挿入

5、揮発性と同期の違い

まず、スレッドセーフの2つの側面、つまり実行制御とメモリの可視性を理解する必要があります。

実行制御の目的は、コードの実行(シーケンス)を制御し、同時に実行できるかどうかを制御することです。

メモリの可視性は、メモリ内の他のスレッドに対するスレッド実行結果の可視性を制御します。Javaメモリモデルの実装によると、スレッドが具体的に実行されると、スレッドは最初にメインメモリデータをスレッドローカル(CPUキャッシュ)にコピーし、操作が完了した後、結果をスレッドローカルからメインメモリにフラッシュします。

同期キーワードは、実行制御の問題を解決します。これにより、他のスレッドが現在のオブジェクトの監視ロックを取得できなくなり、現在のオブジェクトの同期キーワードで保護されたコードブロックに他のスレッドがアクセスできなくなり、同時に実行できなくなります。さらに重要なことに、同期するとメモリバリアも作成されます。メモリバリア命令は、すべてのCPU操作結果がメインメモリに直接フラッシュされるようにし、操作のメモリの可視性を確保し、最初にこのロックを取得するすべてのスレッドを作成します。操作、その後ロックを取得したスレッドの操作の前に発生します。

発生前の理解については、この記事 [並行処理の重要な原則] を参照してください。発生前の理解と適用

volatileキーワードはメモリの可視性の問題を解決するため、揮発性変数へのすべての読み取りと書き込みはメインメモリに直接ブラッシングされ、変数の可視性が保証されます。このようにして、可変の可視性を必要とし、読み取り順序の要件を必要としないいくつかの要件を満たすことができます。

volatileキーワードを使用すると、元の変数(ブール、ショート、int、ロングなど)の操作のアトミック性のみを実現できますが、揮発性では複合操作のアトミック性を保証できないため、特別な注意が必要です。

volatileキーワードの場合、次の条件がすべて満たされた場合にのみ使用できます。

  1. 変数への書き込みは、変数の現在の値に依存しません。または、単一のスレッドのみが変数の値を更新することを保証できます。
  2. 変数は他の変数との不変条件に含まれていません

揮発性と同期の違い

  • volatileの本質は、レジスター(作業メモリー)の現在の変数の値が不明確であり、メインメモリーから読み取る必要があることをjvmに伝えることです;同期は現在の変数をロックすることであり、現在のスレッドのみが変数にアクセスでき、他のスレッドはブロックされます。
  • volatileは変数レベルでのみ使用でき、同期は変数、メソッド、およびクラスレベルで使用できます
  • 揮発性は変数の変更の可視性のみを実現でき、原子性は保証できませんが、同期は変数の変更の可視性と原子性を保証できます
  • volatileはスレッドのブロッキングを引き起こしません;同期するとスレッドのブロッキングが生じる可能性があります。
  • volatileとマークされた変数は、コンパイラーによって最適化されません。synchronizedとマークされた変数は、コンパイラーによって最適化される場合があります。

この記事の内容:Javaメモリモデルの詳細な理解(1)-基礎

ダブルチェックロックパターンの危険性を理解したい場合は、次の記事を参照してください: [シングルケースモード] DCLの問題と解決策

元の記事を147件公開 賞賛を70件 30,000回以上の閲覧

おすすめ

転載: blog.csdn.net/TreeShu321/article/details/105470007