JavaRush /Java Blog /Random-JA /スレッドで Java を台無しにすることはできません: パート III - インタラクション
Viacheslav
レベル 3

スレッドで Java を台無しにすることはできません: パート III - インタラクション

Random-JA グループに公開済み
スレッド対話の機能の簡単な概要。前回は、スレッドがどのように相互に同期するかを調べました。今回は、スレッドが相互作用するときに発生する可能性のある問題を掘り下げ、それらを回避する方法について説明します。より深く学ぶために役立つリンクもいくつか提供します。 スレッドで Java を台無しにすることはできません: パート III - インタラクション - 1

導入

したがって、Java にはスレッドが存在することがわかっています。これについては、「スレッドは Java を台無しにすることができない: パート I - スレッド」というレビューで読むことができます。また、スレッドは相互に同期できることがわかります。これについては、「レビュー」で取り上げました。スレッドは Java をスポイルできません 「スポイル: パート II - 同期」スレッドがどのように相互作用するかについて説明します。彼らはどのようにして共通のリソースを共有するのでしょうか? これにはどのような問題が考えられますか?

デッドロック

最悪の問題はデッドロックです。2 つ以上のスレッドが互いを永遠に待機することをデッドロックと呼びます。Oracle の Web サイトにある「デッドロック」の概念の説明を例に挙げてみましょう。
public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s has bowed to me!%n",
                    this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s has bowed back to me!%n",
                    this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse = new Friend("Alphonse");
        final Friend gaston = new Friend("Gaston");
        new Thread(() -> alphonse.bow(gaston)).start();
        new Thread(() -> gaston.bow(alphonse)).start();
    }
}
ここでのデッドロックは最初は表示されないかもしれませんが、プログラムの実行がスタックしている場合は、次のコマンドを実行してくださいjvisualvmスレッドで Java を台無しにすることはできません: パート III - インタラクション - 2プラグインが JVisualVM にインストールされている場合 ([ツール] -> [プラグイン] 経由)、デッドロックが発生した場所を確認できます。
"Thread-1" - Thread t@12
   java.lang.Thread.State: BLOCKED
    at Deadlock$Friend.bowBack(Deadlock.java:16)
    - waiting to lock &lt33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
スレッド 1 はスレッド 0 からのロックを待っています。なぜこれが起こるのでしょうか? Thread-1実行を開始し、メソッドを実行しますFriend#bow。キーワード でマークされていますsynchronized。つまり、 でモニターを選択しますthis。メソッドの入り口で、別の へのリンクを受け取りましたFriend。ここで、スレッドはThread-1別の でメソッドを実行しFriend、それによってそのスレッドからもロックを取得したいと考えています。しかし、別のスレッド (この場合Thread-0) がメソッドに入ることができた場合bow、ロックはすでにビジーでThread-1待機状態になっておりThread-0、その逆も同様です。ブロッキングは解決不可能なのでデッド、つまりデッドです。死のグリップ(解放できない)と、そこから逃れることのできないデッドブロックの両方。デッドロックのトピックについては、ビデオ「デッドロック - 同時実行 #1 - 高度な Java 」をご覧ください。

ライブロック

デッドロックがあるなら、ライブロックもあるでしょうか? はい、あります) ライブロックとは、スレッドが表面的には生きているように見えますが、同時に何もできないことを意味します。彼らが仕事を続けようとする条件は満たされない。本質的に、ライブロックはデッドロックに似ていますが、スレッドはモニターを待っているシステム上で「ハング」せず、常に何かを実行しています。例えば:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class App {
    public static final String ANSI_BLUE = "\u001B[34m";
    public static final String ANSI_PURPLE = "\u001B[35m";

    public static void log(String text) {
        String name = Thread.currentThread().getName(); //like Thread-1 or Thread-0
        String color = ANSI_BLUE;
        int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
        if (val != 0) {
            color = ANSI_PURPLE;
        }
        System.out.println(color + name + ": " + text + color);
        try {
            System.out.println(color + name + ": wait for " + val + " sec" + color);
            Thread.currentThread().sleep(val * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Lock first = new ReentrantLock();
        Lock second = new ReentrantLock();

        Runnable locker = () -> {
            boolean firstLocked = false;
            boolean secondLocked = false;
            try {
                while (!firstLocked || !secondLocked) {
                    firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
                    log("First Locked: " + firstLocked);
                    secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
                    log("Second Locked: " + secondLocked);
                }
                first.unlock();
                second.unlock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        new Thread(locker).start();
        new Thread(locker).start();
    }
}
このコードが成功するかどうかは、Java スレッド スケジューラがスレッドを開始する順序によって決まります。それが最初に開始するとThead-1、Livelock が取得されます。
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
この例からわかるように、両方のスレッドが交互に両方のロックを取得しようとしますが、失敗します。さらに、彼らは行き詰まりには陥っていません。つまり、見た目にはすべて問題なく、仕事をしています。 スレッドで Java を台無しにすることはできません: パート III - インタラクション - 3JVisualVM によれば、スリープ期間とパーク期間がわかります (これは、スレッドの同期について説明したように、スレッドがロックを占有しようとするとパーク状態になります)。ライブロックのトピックについては、「Java - Thread Livelock」の例を参照してください。

飢餓

マルチスレッドを使用する場合、ブロッキング (デッドロックとライブロック) に加えて、スターベーションまたは「スターベーション」という別の問題があります。この現象は、スレッドがブロックされないという点でブロッキングとは異なりますが、全員に十分なリソースがないだけです。したがって、一部のスレッドはすべての実行時間を引き継ぎますが、他のスレッドは実行できません。 スレッドで Java を台無しにすることはできません: パート III - インタラクション - 4

https://www.logicbig.com/

詳しい例は、「 Java - Thread Starvation and Fairness 」にあります。この例では、Starvation でスレッドがどのように動作するか、また Thread.sleep から Thread.wait への小さな変更 1 つで負荷を均等に分散できる方法を示します。 スレッドで Java を台無しにすることはできません: パート III - インタラクション - 5

競合状態

マルチスレッドを使用する場合、「競合状態」のようなものが発生します。この現象は、スレッドがスレッド間で特定のリソースを共有しており、この場合には正しい動作が提供されないような方法でコードが記述されているという事実にあります。例を見てみましょう:
public class App {
    public static int value = 0;

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                int oldValue = value;
                int newValue = ++value;
                if (oldValue + 1 != newValue) {
                    throw new IllegalStateException(oldValue + " + 1 = " + newValue);
                }
            }
        };
        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();
    }
}
このコードは、最初はエラーを生成しない可能性があります。そして、それは次のようになります:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
    at App.lambda$main$0(App.java:13)
    at java.lang.Thread.run(Thread.java:745)
ご覧のとおり、割り当て中にnewValue問題が発生し、newValueさらに問題が発生しました。競合状態のスレッドの一部は、valueこれら 2 つのチーム間でなんとか変化しました。ご覧のとおり、スレッド間の競合が発生しています。ここで、金銭取引で同様の間違いを犯さないことがいかに重要であるかを想像してみてください。例と図は、「 Java スレッドで競合状態をシミュレートするコード」からも参照できます。

揮発性

スレッドの相互作用について言えば、キーワードに特に注目する価値がありますvolatile。簡単な例を見てみましょう。
public class App {
    public static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Runnable whileFlagFalse = () -> {
            while(!flag) {
            }
            System.out.println("Flag is now TRUE");
        };

        new Thread(whileFlagFalse).start();
        Thread.sleep(1000);
        flag = true;
    }
}
最も興味深いのは、高い確率でそれが機能しないということです。新しいスレッドには変更が反映されませんflag。これを修正するには、フィールドにflagキーワードを指定する必要がありますvolatile。方法と理由?すべてのアクションはプロセッサによって実行されます。ただし、計算結果はどこかに保存する必要があります。この目的のために、プロセッサ上にはメイン メモリとハードウェア キャッシュがあります。これらのプロセッサ キャッシュは、メイン メモリにアクセスするよりも高速にデータにアクセスするための小さなメモリのようなものです。しかし、すべてに欠点もあります。キャッシュ内のデータは最新ではない可能性があります (上記の例のように、フラグ値が更新されなかった場合)。したがって、キーワードはvolatile変数をキャッシュしたくないことを JVM に伝えます。これにより、すべてのスレッドで実際の結果を確認できるようになります。これは非常に単純化された式です。このトピックについては、 「 JSR 133 (Java Memory Model) FAQvolatile 」の翻訳を読むことを強くお勧めします。また、「 Java メモリ モデル」と「Java Volatile Keyword 」という資料について詳しく読むことをお勧めします。さらに、これは可視性に関するものであり、変更の原子性に関するものではないことを覚えておくことが重要です。「競合状態」からコードを取得すると、IntelliJ Idea にヒントが表示されます。 このインスペクション (Inspection) は、 2010 年のリリース ノートに記載されていた問題IDEA-61117の一部として IntelliJ Idea に追加されました。 volatileスレッドで Java を台無しにすることはできません: パート III - インタラクション - 6

原子性

アトミック操作は分割できない操作です。たとえば、変数に値を割り当てる操作はアトミックです。残念ながら、インクリメントはアトミックな操作ではありません。増分には、古い値を取得し、それに 1 を追加し、値を保存するという 3 つの操作が必要です。なぜ原子性が重要なのでしょうか? インクリメントの例では、競合状態が発生すると、いつでも共有リソース (共有値) が突然変化する可能性があります。さらに、 や などの 64 ビット構造もアトミックではないことが重要longですdouble。詳細については、「64 ビット値の読み取りおよび書き込み時にアトミック性を確保する」を参照してください。アトミック性に関する問題の例を次の例に示します。
public class App {
    public static int value = 0;
    public static AtomicInteger atomic = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                value++;
                atomic.incrementAndGet();
            }
        };
        for (int i = 0; i < 3; i++) {
            new Thread(task).start();
        }
        Thread.sleep(300);
        System.out.println(value);
        System.out.println(atomic.get());
    }
}
アトミックを扱うための特別なクラスではInteger常に 30000 が表示されますが、valueそれは時々変わります。このトピックには「 Java のアトミック変数の概要」という短い概要があります。Atomic は比較交換アルゴリズムに基づいています。詳細については、Habré の記事「ロックフリー アルゴリズムの比較 - JDK 7 と 8 の例を使用した CAS と FAA 」、または Wikipedia の「交換との比較」の記事を参照してください。 スレッドで Java を台無しにすることはできません: パート III - インタラクション - 8

http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html

前に起こる

面白くて不思議な事があります - 前に起こることです。フローについて言えば、それについても読む価値があります。Happens Before 関係は、スレッド間のアクションが表示される順序を示します。いろいろな解釈や解釈があります。このトピックに関する最新のレポートの 1 つが次のレポートです。
このビデオでは何も語らない方が良いかもしれません。ということで、動画へのリンクだけ貼っておきます。「Java - Happens-before relationship を理解する」を読むことができます。

結果

このレビューでは、スレッド インタラクションの機能を検討しました。発生する可能性のある問題と、それらを検出して排除する方法について説明しました。このトピックに関する追加資料のリスト: #ヴィアチェスラフ
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION