blog

なんてこった、セマフォにはこんなに大きな穴が開いている!

リズムのバカバカしさ\n\n\n\n\n冷たいテクニカル・ライティングに色彩を注入するのが目的。\n\n\nさて、この問題のばかばかしさから始めましょう。\n上の写真は、先週末「Summer of B...

Mar 17, 2020 · 9 min. read
シェア

これが 59回目の 投稿です。

リズムのバカバカしさ

こんにちは、WHY兄さんです。

先週、ある記事を書いたところ、偶然にも皆さんのツボを突いてしまったようで、そのうちの一人が私の記事を転載してくれました。

でも本当は、私は技術系ブロガーで、たまに生活関連のことを書いています。だから、これは技術に戻ります。

私は何度もこのような記事を書いていますが、読者から「バカバカしいところを読んで辞めなさい。

さて、この問題のばかばかしさから始めましょう。

上の写真は、先週末にサマー・オブ・バンドを見ていたときに撮ったものです。

私はこのバンドが大好きです。

私が彼らを聴いたのは中学生の頃だったと思いますが、当時はカセットテープがほとんど廃れかけていて、コンパクトディスクの時代に突入した頃で、コンパクトディスクには数十曲入っていて、DVDで初めて聴いたのが「A Lifetime of You」で、曲を聴いた時にとてもクリーンで素晴らしい曲だと感じたのを覚えています。

そして、自分の歌詞集に一字一句コピーしてください。

当時のDVDにはA-Bを何度も再生できる機能があったので、一行ずつ覚えました。

その時、李堅は湖のように澄んだ大きな瞳をしていました。

スクール・バラードの最高峰のひとつと呼べる曲です。

サマー・オブ・ミュージック」のステージで「青春にさよなら」を歌い、太った23歳からは「中年で脂ぎってる」と言われ、別のプロの音楽ファンからは「なんで40代の人がまだ『青春にさよなら』を歌うの?第1ステージで敗退。第1ステージで敗退。

見るからに素晴らしい作戦です。

これはどうして脂っこい?どうして40代の人が青春の別れを歌えないの?男は死ぬまで10代なんですよ。ガキ、音楽やってるときはしゃべれなかったくせに。

ステージを去る彼らの姿に、苦い思い、本当に若さゆえの別れを感じました。

ウォーターウッドは何も悪くない、悪いのはステージ。

さて、記事に戻りましょう。

一緒に問題を見てください。

数日前、読者からこのリンクの中にあるコードへのリンクを送ってもらいました:

"https://.io/t/topic/3911"

コードは以下のようになります:

10行目のpermitというパラメータに注目してください:

なぜコードが2を出すのか分かりません。後でコードに簡単な変更を加えます。

セマフォが何をするものかを知っていれば、上のコードを見て、なぜデッドロックを引き起こすのかを知ることができます。

とにかく、非常に息の詰まるような低レベルのミスなのですが、実は何度読み返しても気づかなかったのです。

セマフォの役割をご存じない方は、こちらを見てください。簡単に説明します。

一般的にセマフォと呼ばれるセマフォは、指定されたリソースに同時にアクセスするスレッドの数を制御するために使用されます。

もしあなたがセマフォを理解していないなら、上記のコードは理解できないでしょう。

例えば、3台分しかない高級駐車場。

今は駐車場がないので、最大で何台停められるのでしょうか?

はい、入り口の残台数表示には「駐車スペース残り3台」と表示されています。

このとき、来て駐車しようとする方法は3つありました。

走っている車は、兄のロールスロイス、趙斯のブガッティ、劉能、謝光勲、仲良しの二人組はフェラーリ:

その時、「賛美の道」から来た趙思が先に車を走らせ、停車。

入り口の駐車場には、残り2台と表示されています。

劉能、謝光勲が到着し、ちょうど2つのスペースが残っていることを発見したので、仲の良い友人たちは手をつないで一緒に駐車しました。

エントランスの駐車スペースは、残り0台と表示されています。

私もあっという間に到着し、駐車スペースがないことに気づき、どうしよう?入り口で待つしかありません。

あっという間に用事を済ませ、ブガッティで走り去った趙氏。

エントランスの駐車場は、残り1台と表示されています。

急いで駐車場に入りました。

エントランスの駐車スペースは、残り0台と表示されています。

上のコードは、まさにそのようなことを記述しようとしています。

しかし、質問者の説明によると、"実行時に、スレッドAだけが実行を終了し、そのスレッドBとスレッドCが沈黙することがある "とのことです。

それがスタイルです:

なぜ駐車できないのですか?彼はデッドロックだと疑っているんです。 ちょっと意味不明な疑いですね。

まず、デッドロックの4つの必要条件を思い出してください:

  • 相互排他条件:リソースは一度に1つのプロセスによってのみ使用可能。他のプロセスがリソースを要求した場合、要求したプロセスは待つことしかできません。

  • リクエスト&ホールド条件:プロセスはすでに少なくとも1つのリソースを保持していますが、他のプロセスによってすでに占有されているリソースに対して新たなリクエストを行います。

  • 不可侵条件:あるプロセスが獲得したリソースは、それが使用されるまで、他のプロセスによって強制的に奪われることはありません。

  • Cyclic Waiting Condition(循環待機条件): リソースを待機する複数のプロセスの間の関係。

これらの4つの条件はデッドロックの必要条件です。必要条件とは、デッドロックがある限り、これらの条件がすべて成立しなければならないということです。

そして、それを分析した結果、デッドロックの必要条件が満たされていないことが判明しました。では、なぜこのようなことが起こるのでしょうか?

上記のシナリオに基づいたコードを書くことから始めましょう。

自分でコードを入力してください。

以下の手順は、基本的に上のスクリーンショットの例のコードに従って変更され、上記の物語に参加し、あなたが直接コピーして貼り付けることができます:

public class ParkDemo {
 public static void main(String[] args) throws InterruptedException {
 Integer parkSpace = 3;
 System.out.println("これだ。 + parkSpace + "早い者勝ちだ!");
 Semaphore semaphore = new Semaphore(parkSpace, true);
 Thread threadA = new Thread(new ParkCar(1, "ブガッティ」、セマフォ)、「ザオ・シー」)。;
 Thread threadB = new Thread(new ParkCar(2, "フェラーリ」、セマフォ)、「劉能、謝光勲」)。;
 Thread threadC = new Thread(new ParkCar(1, "ロールスロイス」、セマフォ)、「なぜ兄弟」)。;
 threadA.start();
 threadB.start();
 threadC.start();
 }
}
class ParkCar implements Runnable {
 
 private int n;
 private String carName;
 private Semaphore semaphore;
 public ParkCar(int n, String carName, Semaphore semaphore) {
 this.n = n;
 this.carName = carName;
 this.semaphore = semaphore;
 }
 @Override
 public void run() {
 try {
 if (semaphore.availablePermits() < n) {
 System.out.println(Thread.currentThread().getName() + "駐車場に来なさい、でも駐車場が足りないから待ってて」);
 }
 semaphore.acquire(n);
 System.out.println(Thread.currentThread().getName() + "自分自身を" + carName + "入るぞ、残りの駐車スペース。:" + semaphore.availablePermits() + " ;
 //模擬駐車の長さ
 int parkTime = ThreadLocalRandom.current().nextInt(1, 6);
 TimeUnit.SECONDS.sleep(parkTime);
 System.out.println(Thread.currentThread().getName() + "自分自身を" + carName + "走り去れ、止まれ" + parkTime + "時間」)。;
 } catch (Exception e) {
 e.printStackTrace();
 } finally {
 semaphore.release(n);
 System.out.println(Thread.currentThread().getName() + "私たちが去った後、残りの駐車スペースは:" + semaphore.availablePermits() + " ;
 }
 }
}

実行後の結果は以下の通り:

今回は予想通りの結果になりました。スレッドのブロックはありませんでした。

では、なぜ「実行時にスレッドAだけが実行され、スレッドBとスレッドCは沈黙する」という現象が起こるのでしょうか?

それはモラルの欠如なのか、それとも歪んだ人間性なのか?掟の中にお連れしましょう:

違いは、残りのパスを取得する方法にあります。上がリンク先のコード、下が自分で書いたコードです。

正直なところ、リンクの中のコードは最初、私の固い目で1分間コンパイルしたのですが、問題がわかりませんでした。

実際にコードをIDEAに突っ込んで実行してみると、Bスレッドが先に実行されると、AスレッドとCスレッドが実行されることがわかりました。Aスレッドが先に実行されると、BスレッドとCスレッドは実行されません。

私は混乱し、何度も何度もそれを分析しました!そこで私は深く考え込んでしまいました:

しばらくすると、掃除のおばさんがゴミを回収しにやってきて、「ハーイ、ハンサムちゃん、このレッドブルのボトル、飲み終わったでしょ?ボトルを運びますよ"そして画面をちらっと見て、残りのライセンスを取得するコードの行を指差し、私にこう言いました。

System.out.println("残りのライセンス: " + semaphore.drainPermits());

残りの利用可能なライセンスを取得するメソッドはdrainPermitsであるため、スレッドAが呼び出しを終了した後、残りのライセンスは0になり、リリースを実行した後、ライセンスは1になります。

これも公平なロックなので、スレッドBが先に侵入してキューに並んだ場合、スレッドBが実行するのに十分なライセンスが残っておらず、待機します。 スレッドCは実行する機会を得られません。

残りの利用可能なライセンスを取得するメソッドをavailablePermitsメソッドに置き換えた後の出力は正常です:

これは本当に些細なことです。それが当局や野次馬について言われることです。

方法論の説明

セマフォについてよく知らない人の多くは、最初の2つのパートを読んでもまだ少し混乱しているのではないでしょうか。

大丈夫です、疑問はこの小節ですべて解消されます。

上記のテストケースでは、セマフォの4つのメソッドだけが使用されています:

  • availablePermits: 残りの利用可能な許可証を取得します。

  • drainPermits : 残りの利用可能なライセンスを取得します。

  • release: 指定した数のライセンスをリリースします。

  • acquire:指定されたライセンス数の申請。

まず、availablePermitsメソッドとdrainPermitsメソッドの違いを見てみましょう:

この2つの場所の文書説明はちょっとした言葉遊び。気をつけないと取り込まれてしまいますよ。

availablePermitsはライセンスの残り数を見るだけで、drainPermitsは残りのライセンスをすべて取得します。

つまり、上記のシナリオでは、両方のメソッドの戻り値は同じですが、内部処理は完全に内部的に異なっています:

この発見を掃除のおばさんに報告すると、彼女は優しく微笑みながら、"drainPermitsの前にdrainの意味を調べてみたらどうかしら、坊や?"と。

確認した後、英語IVの涙が残りました:

意味は名前を見てください。生徒の皆さん、プログラミングをする上で、やはり英語はとても重要だということがお分かりいただけたと思います。

次に、まずリリースの方法を見てみましょう。

この方法は、指定された数のライセンスを解放することです。解放するということは、許可証の数が増えるということです。劉能と謝光勲がそれぞれのフェラーリを駐車スペースから出して駐車場から走り去り、その時点で駐車場の駐車スペースが2台分増えるのと同じです。

この方法のエッセンスは599行目から602行目に書かれています:

この一文は非常に重要です。リリース操作を実行するスレッドは、acquireメソッドを実行するスレッドである必要はない、ということです。

開発者は、シナリオに応じて、セマフォが正しく使用されていることを確認する必要があります。

ここでリリース操作をする場合、finallyのコードブロックに入れなければ実行できないことは誰でも知っています。しかし、この知識こそが落とし穴を踏みやすい場所であり、問題の種類を調べるのも非常に難しいのです。

最終ブロックに入れるのは確かにいいアイデアですが、入れ方の問題です。

次のセクションの例とacquireメソッドを組み合わせてみます:

獲得メソッドは、私が赤枠で囲った部分に主眼を置いています。

このメソッドのソース・コードからわかるように、InterruptException がスローされます。このことを覚えておいて、次のセクションでシナリオの議論をしましょう。

release誤用の大穴

または駐車場のシーンに持ち込みます。趙斯と私が最初に車を駐車したとすると、今度は劉能、謝光君、彼らが来て、駐車スペースが十分ではないことを発見し、二人の仲良し、ただ待って、一緒に駐車する必要があります

しばらく待っても出てこないので、おじいさんの車を見ていたらドアから出てきて、"まだ長い時間待たないといけないと思うから、待たずに早く行きなさい "と言われました。

それで彼らは車を走らせたんです。

このシーンのコード全体です:

public class ParkDemo {
 public static void main(String[] args) throws InterruptedException {
 Integer parkSpace = 3;
 System.out.println("これだ。 + parkSpace + "早い者勝ちだ!");
 Semaphore semaphore = new Semaphore(parkSpace, true);
 Thread threadA = new Thread(new ParkCar(1, "ブガッティ」、セマフォ)、「ザオ・シー」)。;
 Thread threadB = new Thread(new ParkCar(2, "フェラーリ」、セマフォ)、「劉能、謝光勲」)。;
 Thread threadC = new Thread(new ParkCar(1, "ロールスロイス」、セマフォ)、「なぜ兄弟」)。;
 threadA.start();
 threadC.start();
 threadB.start();
 //アナログモンクレール
 threadB.interrupt();
 }
}
class ParkCar implements Runnable {
 private int n;
 private String carName;
 private Semaphore semaphore;
 public ParkCar(int n, String carName, Semaphore semaphore) {
 this.n = n;
 this.carName = carName;
 this.semaphore = semaphore;
 }
 @Override
 public void run() {
 try {
 if (semaphore.availablePermits() < n) {
 System.out.println(Thread.currentThread().getName() + "駐車場に来なさい、でも駐車場が足りないから待ってて」);
 }
 semaphore.acquire(n);
 System.out.println(Thread.currentThread().getName() + "自分自身を" + carName + "立ち止まる。," + "残りの駐車スペース:" + semaphore.availablePermits() + " ;
 //模擬駐車の長さ
 int parkTime = ThreadLocalRandom.current().nextInt(1, 6);
 TimeUnit.SECONDS.sleep(parkTime);
 System.out.println(Thread.currentThread().getName() + "自分自身を" + carName + "走り去れ、止まれ" + parkTime + "時間」)。;
 } catch (InterruptedException e) {
 System.err.println(Thread.currentThread().getName() + "ドアの前にいたおじいちゃんに止められたよ」)。;
 } finally {
 semaphore.release(n);
 System.out.println(Thread.currentThread().getName() + "私たちが去った後、残りの駐車スペースは:" + semaphore.availablePermits() + " ;
 }
 }
}

コードを見ても何も問題はありませんが、実行してみると、このようなことが起こる可能性があることがわかります:

HYが去ったら、残りの駐車スペースが5台に?私がロールスロイスに乗って駐車スペースを開発したのでしょうか?

ログを楽しみにして、元の劉寧、謝Guangkun左、残りの駐車スペース3を示していることがわかりました。

そこに問題があるのです。

そして、この場所に対応するコードは次のようになります:

ちょっとだけ夜が明けたような気がします。

50行目でInterruptedExceptionがスローされ、明らかにライセンスを取得していないスレッドがreleaseメソッドを実行したため、ライセンスが増加しました。

内部の例では、劉能は、謝Guangkunの車はまだ駐車していない、ディスプレイのドアが2つの駐車スペースを追加したときに離れて行きます。

これが落とし穴であり、あなたのコードに潜むバグです。

では、どうやって解決するのですか?

答えはもう明らかで、ここはキャッチアップする必要があり、割り込み例外が発生した場合は、直接戻ります:

駐車スペースは3台分しかありませんでした:

免許を取得したばかりで、中断された場合はどうなるのでしょうか?

ソースコードを見てください。 答えはソースコードの中にあります。

InterruptedException がスローされた後、このスレッドに割り当てられているすべてのライセンスは、release メソッドが呼び出された場合と同様に、ライセンスを必要とする他のスレッドに割り当てられます。

強化リリース

上記の問題を分析すると、ライセンスを取得しなかったスレッドがリリース・メソッドを呼び出したことが原因であることがわかります。

穴に足を踏み入れやすい設定だと思います。単純に大きな落とし穴です!

リリース・メソッドは、そのメソッドを取得したスレッドからのみ呼び出すように拡張することができます。

このトリックは、Java高並行プログラミング解説-並行処理コア・ライブラリの深い理解で学びました:

3.4.4項 「リリースを強化するためのセマフォの拡張」。

ライセンスを取得するメソッドが変更され、成功するとキューに置かれるようになりました:

releaseメソッドの内部は、現在のスレッドがキューにあるかどうかを確認するために最初に実行されるように変更されています:

温かい気持ちの断片もあります:

この本は今でもよく書けており、お勧めです。

最後に一言。

読んでくださってありがとうございます。

私はなぜ、コードによって遅延文学クリエイター、お兄ちゃんではなく、共有するのが好きですが、温かく、材料四川の良い男です

Read next

バイトフリップブラックボックス

車はああを要約するために、もちろん転覆し、そうでなければ再び転覆は離れて駆動することができますか?\n質問1バイナリツリー\nアルゴリズムJSアルゴリズム木トラバーサル\n\nクラスノード\n クラスノード\n クラスノード{ {

Mar 17, 2020 · 3 min read