インタビュー中に私は「スレッドセーフとは何か」について話しました、そしてインタビュアーは私に親指を上げました

スレッドセーフではありません

インタビュアーは、「スレッドセーフとは何ですか?」と尋ねました。うまく答えられない場合は、見下ろしてください。

論語には「勉強と卓越性が公務につながる」ということわざがあります。「よく学べば公務員になれる」と多くの人が考えていると思います。しかし、この理解は間違っています。文学的に見えることを忘れないでください。

同様に、「スレッドセーフ」はスレッドセーフではなく、メモリセーフを意味します。なぜそう言うのですか?これはオペレーティングシステムに関連しています。

現在の主流のオペレーティングシステムはマルチタスクです。つまり、複数のプロセスが同時に実行されます。安全性を確保するために、各プロセスは自分に割り当てられたメモリスペースにのみアクセスでき、他のプロセスにはアクセスできません。これはオペレーティングシステムによって保証されています。

各プロセスのメモリ空間には、通常ヒープ(メモリ)と呼ばれる特別なパブリック領域があります。プロセス内のすべてのスレッドがこの領域にアクセスできます。これが問題の潜在的な原因です。

スレッドがデータを半分に処理し、疲れを感じたため、休憩を取り、処理を続行するために戻ったが、データが変更されたことがわかり、データが残ったときの状態ではなかったとします。他のスレッドによって変更される可能性があります。

たとえば、あなたが住んでいるコミュニティをプロセスとして考え、コミュニティの道路/緑化は公共エリアに属しています。あなたは地面に10,000元を投げ、家に帰って眠ります。目覚めた後、あなたはそれを拾い上げて、お金がなくなっていることに気付く予定です。他人に奪われる可能性があります。

公共の場を行き来する人がいるので、物を片付けるのは警戒しないと危険です。同じことがメモリにも当てはまります。

したがって、スレッドセーフとは、ヒープメモリ内のデータが誤って変更されるリスクを指します。これは、どのスレッドからでも制限なくアクセスできるためです。

つまり、ヒープメモリスペースは、保護メカニズムのないマルチスレッドにとって安全でない場所です。これは、入力したデータが他のスレッドによって「破壊」される可能性があるためです。

私たちは何をすべきか?問題解決プロセスは実際にはトレードオフのプロセスであり、ソリューションが異なれば焦点も異なります。

私的なことは他人に知られてはいけません

実際には、1万元を隠して関係のない人から遠ざける人が多いので、幹線道路に投げることはできません。このお金はあなたの私有財産だからです。

プログラムでも同じことが言えるため、オペレーティングシステムはスレッドごとに独自のメモリスペース(通常はスタックメモリと呼ばれます)を割り当て、他のスレッドはそれにアクセスできません。これは、オペレーティングシステムによっても保証されています。

一部のデータが特定のスレッドによってのみ使用され、他のスレッドが操作できないか、操作する必要がない場合、これらのデータをスレッドのスタックメモリに入れることができます。より一般的なものはローカル変数です。

double avgScore(double[] scores) {
    
    
    double sum = 0;
    for (double score : scores) {
    
    
        sum += score;
    }
    int count = scores.length;
    double avg = sum / count;
    return avg;
}

変数sum、count、およびavgはすべてローカル変数であり、すべてスレッドスタックメモリに割り当てられます。

スレッドAがこのメソッドを実行すると、これらの変数はAのスタックメモリに割り当てられます。同時に、Bスレッドもこのメソッドを実行し、これらの変数もBのスタックメモリに割り当てられます。

つまり、これらのローカル変数は、各スレッドのスタックメモリに割り当てられます。スレッドのスタックメモリにはそれ自体でしかアクセスできないため、スタックメモリ内の変数はそれ自体にのみ属し、他のスレッドはそれを認識しません。

みんなの家が自分だけのものであるように、他の人が入ることはできません。ですから、家に1万元入れたら、誰にもわかりません。そして、それは通常、居間のテーブルの上ではなく、部屋に置かれます。

ですから、他の人はそれを知ることができないので、あなた自身のものをあなた自身の私有地に置くことは安全です。そして、その場所がプライベートであればあるほど良いです。

みんなをつかまないでください、みんながシェアを持っています

[記事の利点]編集者は私自身のlinuxC / C ++言語交換グループ832218493を推奨しています。共有するのに適していると思う学習本とビデオ資料をいくつかまとめました。必要に応じて追加できます!〜!
ここに画像の説明を挿入

あなたが賢いと信じて、あなたは上記の解決策が「場所」に基づいていることを発見しました。物を置く「場所」は自分だけが知っている(または到達できる)ので、物は安全であり、この安全は「場所」によって保証されます。

プログラムでは、メソッドのローカル変数に対応します。ローカル変数が安全である理由は、ローカル変数が定義されている「場所」がメソッド内にあるためです。このようにしてセキュリティは実現されますが、その使用範囲はこの方法に限定され、他の方法は不要になります。

実際には、変数を複数のメソッドで使用する必要がある場合がよくあります。現時点では、変数の「場所」をメソッド内で定義することはできませんが、メソッドの外部に配置する必要があります。つまり、(メソッドの)ローカル変数から(クラスの)メンバー変数への変更は、実際には「位置」が変更されています。

次に、主流のプログラミング言語の規制に従って、クラスのメンバー変数をスレッドのスタックメモリに割り当てることはできなくなりますが、パブリックヒープメモリに割り当てる必要があります。実際、メモリ内の変数の「場所」は、プライベートエリアからパブリックエリアに変更されました。したがって、潜在的なセキュリティリスクも発生します。

公共エリアの物の安全を確保する方法は?答えは、それをつかまないでください、誰もがシェアを持っています。路上でミネラルウォーターを無料で配布し、10,000人が来たが、ボトル入り飲料水が1,000本しかない場合、想像できるように、群がってシーンを失ったとします。でも、10万本の水を持っていると、水がたくさんあるのがみんなにわかりますので、1本ずつ並べていくので安心です。

もっとたくさんあると当然無価値ですが、別の見方をすれば安全です。路上で共有されている自転車は、どこにでもたくさんあり、すべて同じように見えるため、今では非常に安全です。そのため、妨害工作員でさえあきらめています。だから、何かを安全にするために、それをクレイジーにコピーしてください。

プログラムに戻って、パブリック領域のヒープメモリ内のデータをスレッドごとに安全にするために、各スレッドはそれをコピーし、各スレッドは他のスレッドに影響を与えることなく独自のコピーのみを処理します。スレッドは安全です。ご想像のとおり、私が表現したいのはThreadLocalクラスです。

class StudentAssistant {
    
    

    ThreadLocal<String> realName = new ThreadLocal<>();
    ThreadLocal<Double> totalScore = new ThreadLocal<>();

    String determineDegree() {
    
    
        double score = totalScore.get();
        if (score >= 90) {
    
    
            return "A";
        }
        if (score >= 80) {
    
    
            return "B";
        }
        if (score >= 70) {
    
    
            return "C";
        }
        if (score >= 60) {
    
    
            return "D";
        }
        return "E";
    }

    double determineOptionalcourseScore() {
    
    
        double score = totalScore.get();
        if (score >= 90) {
    
    
            return 10;
        }
        if (score >= 80) {
    
    
            return 20;
        }
        if (score >= 70) {
    
    
            return 30;
        }
        if (score >= 60) {
    
    
            return 40;
        }
        return 60;
    }
}

この学生アシスタントクラスには、realNameとtotalScoreの2つのメンバー変数があり、どちらもThreadLocal型です。各スレッドはコピーを作成し、実行時にローカルに保存します。

スレッドが「ZhangSan」と「90」を実行している場合、これら2つのデータ「ZhangSan」と「90」はAスレッドオブジェクト(Threadクラスのインスタンスオブジェクト)のメンバー変数に格納されます。この時点でBスレッドも実行されているとすると、「Li Si」と「85」であり、2つのデータ「LiSi」と「85」がBスレッドオブジェクト(のインスタンスオブジェクト)のメンバー変数に格納されます。スレッドクラス)アップ。

スレッドクラス(Thread)には、Mapタイプと同様に、特にThreadLocalデータを格納するために使用されるメンバー変数があります。論理的な所属という点では、これらのThreadLocalデータは、Threadクラスのメンバー変数レベルに属しています。「場所」の観点から、これらのThreadLocalデータはパブリックエリアのヒープメモリに割り当てられます。

率直に言って、ヒープメモリにデータのNコピーをコピーすることであり、各スレッドは1コピーを要求します。同時に、各スレッドは独自の共有のみを再生でき、許可されないことが規定されています。他の人に影響を与えます。

これらのN個のデータはまだパブリックエリアのヒープメモリに保存されていることに注意してください。よく聞かれる「スレッドローカル」は論理的な従属の観点からです。これらのデータとスレッドは1対1で対応します。それらがスレッド自体の「テリトリー」のものになっている場合。実際、データの「場所」の観点から、それらはすべてパブリックヒープメモリにありますが、スレッドによってのみ要求されます。この点を強調したいと思います。

実際、それは路上で共有される自転車のようなものです。たった1人しかいなかったので、みんな急いで乗って、いつも問題がありました。これからN台の車をコピーします。各人に1台ずつ、それぞれが独自に乗ります。問題は解決しました。共有自転車はデータであり、あなたはスレッドです。乗車中、自転車は論理的にはあなたのものですが、各コミュニティのゲートに「シェアサイクル、入場禁止」と掲示されているため、場所的にはまだ通りの公共エリアにあります。ハハハッハッハ。

自転車の共有はThreadLocalと非常に似ていますか?繰り返しになりますが、ThreadLocalはデータの一部をNコピーコピーし、各スレッドは1つのコピーを要求し、それぞれが相互に影響を与えることなく独自に再生します。

見ることしかできず、触れることはできません

公共エリアに置かれたものには潜在的なセキュリティリスクがあるだけで、必ずしも安全ではありません。公共の場所に保管されているものもありますが、非常に安全です。たとえば、数百トンの石像を路上に置いた場合、誰もが動かせないのでとても安全です。

もう1つの例は、旅行に行くときに、「見るだけで、触れない」という看板が付いた鉄のフェンスに囲まれた貴重なものを見つけることがよくあります。もちろん、それは少し国際的である可能性があり、「見るだけで、触れないでください」。一目見ただけでは見栄えが悪いので、これも非常に安全です。

プログラムに戻ると、これは読み取りのみが可能で、変更はできない場合です。実際、これらは定数または読み取り専用変数であり、マルチスレッドに対して安全であり、必要に応じて変更することはできません。

class StudentAssistant {
    
    

    final double passScore = 60;
}

たとえば、合格点を60ポイントに設定し、前に決勝を追加して、すべてのスレッドがそれを移動できないようにします。これは非常に安全です。

ここに短いセクションがあります:上記の3つの解決策は実際には「遊びのトリック」です。

一つ目は、自分だけが知って隠れている場所を見つけることです。もちろん安全です。

二つ目は、一人一人に一枚ずつコピーして、お互いに影響を与えずに自分でプレイするというもので、もちろん安全です。

3つ目は、より厳しいもので、読み取りのみが可能であり、変更は禁止されていることを直接規定しています。もちろん、安全です。

彼らは皆、重いものと軽いものを避けようとしていますか?これらの3つの方法のいずれもそれを解決できない場合はどうすればよいですか?心配しないで、読み続けてください。

ルールはありません、そして先入観

上記の3つのスキームは、少し「理想的」です。現実は実際には非常に混沌としていて騒々しく、規則はありません。

たとえば、正午のピーク時にレストランに夕食に行くと、ドアに入った後、空のテーブルが1つしか残っていないことがわかります。最初に注文し、戻ってきたらここに座ってください。食事を注文して戻ってきたとき、他の誰かが主導権を握っていることに気づきました。

テーブルはパブリックエリアのアイテムなので、誰でも座れるので、最初に手に取った人は誰でも座れます。群衆の中で一度見たことがありますが、あなたの顔を覚えていません。

解決策について話す必要はありません。一人が席を見て、他の人が食べ物を注文するようにします。このように、他の人がまた来たとき、「すみません、この席、もう占領しました」と自信を持って言えます。

賢いことですが、あなたは私が何を言おうとしているのか推測していると思います。そうです、それは(相互排除)ロックです。

プログラムに戻って、パブリック領域(ヒープメモリ)のデータを複数のスレッドで操作する場合は、データのセキュリティ(または一貫性)を確保するために、データの横にロックを設定する必要があります。データを操作したい場合は、最初に取得します。ロックについて説明しましょう。

スレッドがデータを確認するために前面に来て、ロックが解放されていて、誰もそれを保持していないことを発見したとします。それで、それはロックを取得し、それからデータを操作し始め、しばらくの間働きました、そしてそれが疲れたとき、それは休みました。

このとき、別のスレッドが来て、ロックが他人に保持されていることを発見しましたが、規定により、ロックを取得できなかったため、データを操作できませんでした。もちろん、待つか、あきらめて、他のことに移ることもできます。

最初のスレッドがあえてスリープ状態になる理由は、そのスレッドが手にロックを持っており、他のスレッドがデータを操作することができないためです。戻ってきてデータを操作し続けると、ロックを解除できます。ロックは再びアイドル状態に戻り、他のスレッドがロックを取得できます。最初にロックを取得し、データを操作するのは誰でもです。

class ClassAssistant {
    
    

    double totalScore = 60;
    final Lock lock = new Lock();

    void addScore(double score) {
    
    
        lock.obtain();
        totalScore += score;
        lock.release();
    }

    void subScore(double score) {
    
    
        lock.obtain();
        totalScore -= score;
        lock.release();
    }
}

クラスの初期スコアを60ポイントとすると、このクラスでは10人の生徒が同時に10の異なる解答プログラムに参加します。各生徒は、正解すると5ポイント、不正解に対して5ポイントを獲得します。 。10人の学生が一緒に働いているので、これは同時の状況でなければなりません。

したがって、ポイントを加算および減算する2つの方法が同時に呼び出され、それらが連携して合計スコアを操作します。データの整合性を確保するために、各操作の前にロックを取得し、操作の完了後にロックを解放する必要があります。

たとえ傷ついたとしても、世界は愛に満ちていると信じてください

最初の例に戻って、まだ地上で10,000元を費やしている場合、それを失うでしょうか?状況にもよりますが、人が行き交う街にいると絶対に負けると言えます。無人地帯に駆け寄って地面に投げれば、絶対に投げないだろうと言えます。

すべてのものが保護なしで公共エリアに置かれていることがわかりますが、結果は大きく異なります。これは、セキュリティの問題が公共エリアの環境条件にも関連していることを示しています。

たとえば、データをパブリックエリアのヒープメモリに配置しましたが、常に1つのスレッドしか存在しないため、シングルスレッドモデルであるため、データは間違いなく安全です。

さらに、2つのスレッドが同じデータを操作し、200のスレッドが同じデータを操作する場合、このデータの安全確率は完全に異なります。スレッドが多いほど、データが安全でない可能性が高くなり、スレッドが少ないほど、データが安全でない可能性が低くなることは確かです。限界の場合を考えてみましょう。つまり、スレッドが1つしかない場合、安全でない確率は0であり、これは安全です。

もう一度表現したいことを推測されたかもしれませんが、そうです、それはCASです。ロックで問題が解決できるので使えると誰もが思っているかもしれませんが、なぜCASが再び現れるのでしょうか。

これは、ロックの取得と解放には一定のコストがかかるためです。スレッド数が特に少ない場合は、データを操作するスレッドが他にまったくない可能性があります。現時点では、ロックの取得と解放が必要です。無駄です。 。

この「大面積で人口がまばらな」状況に対応して、CAS(Compare And Swap)と呼ばれる方法が提案されました。つまり、並行性が小さい場合、誤ってデータが変更される可能性は非常に低いですが、その可能性があり、現時点ではCASを使用しています。

スレッドがデータを操作し、作業の半分を実行し、疲れていて、休憩したいとします。(今日のスレッドの体格はあまり良くないようです)。そのため、データの現在の状態(つまり、データの値)を記録し、ホームに戻ってスリープ状態になります。

起きてから仕事を続けるつもりですが、データが変更されるのではないかと心配なので、就寝前に保存したデータ状態を取り出して、現在のデータ状態と比較します。スリープ中にデータが変更されていないことを確認します。移動後(もちろん、最初に他のデータに変更してから元に戻すことができます。これはABAの問題です)、それを続行します。同じでない場合は、データが変更されており、以前に行った操作が実際には役に立たないことを意味しますので、あきらめて最初からやり直してください。

したがって、CAS方式は、並行性の量が多くない状況、つまり、データが誤って変更される可能性が低い状況に適しています。並行性の量が多い場合、データは確実に変更され、毎回あきらめてから最初からやり直す必要があります。これにはさらにコストがかかります。直接ロックすることをお勧めします。

ABA問題の説明は次のとおりです。就寝前のデータが5、起床後のデータが5の場合、データが変更されていないかどうかは不明です。たぶん、データは最初に8に変更され、次に5に戻されましたが、あなたはそれを知りません。この問題は実際には非常に簡単に解決できます。バージョン番号フィールドを追加するだけで、データが変更されている限り、バージョン番号を1つ増やす必要があると規定されています。

このように、就寝前のデータは5、バージョン番号は0、起床後のデータは5、バージョン番号は0であり、データが変更されていないことを示しています。データが5で、バージョン番号が2の場合、データが2回変更され、最初に他のデータに変更され、次に5に戻されたことを意味します。

ここでのスマートなロックは実際には楽観的なロックであることがわかったと思いますが、前のスキームでのロックの取得と解放は実際には悲観的なロックです。Optimistic Lockは楽観的であり、データが誤って変更されないことを前提としています。変更された場合は、あきらめて最初からやり直します。ペシミスティックロックはペシミスティックな態度を取ります。つまり、データが誤って変更されると想定すると、直接ロックできます。

おすすめ

転載: blog.csdn.net/lingshengxueyuan/article/details/111476564