数日前、テクノロジープラットフォームで誰かが提起したSynchronizedの使用法についての質問を見ました。それは非常に興味深いと思いました。この質問は、実際には3年前に会社にインタビューしたときに出会った本当の質問でした。当時のインタビュアーを知っていたのですが、テストしたかったのですが、あまりよく答えられず、調べて覚えました。
ですから、この質問を見たとき、私はとても親切で、みんなと共有する準備ができていると感じました。
まず、記事を読んだときに問題を再現しやすくするために、直接実行できるコードを提供します。時間がある場合は、コードを取り出して実行することもできます。 :
public class SynchronizedTest {
public static void main(String[] args) {
Thread why = new Thread(new TicketConsumer(10), "why");
Thread mx = new Thread(new TicketConsumer(10), "mx");
why.start();
mx.start();
}
}
class TicketConsumer implements Runnable {
private volatile static Integer ticket;
public TicketConsumer(int ticket) {
this.ticket = ticket;
}
@Override
public void run() {
while (true) {
System.out.println(Thread.currentThread().getName() + "开始抢第" + ticket + "张票,对象加锁之前:" + System.identityHashCode(ticket));
synchronized (ticket) {
System.out.println(Thread.currentThread().getName() + "抢到第" + ticket + "张票,成功锁到的对象:" + System.identityHashCode(ticket));
if (ticket > 0) {
try {
//模拟抢票延迟
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "抢到了第" + ticket-- + "张票,票数减一");
} else {
return;
}
}
}
}
}
プログラムロジックも非常にシンプルで、チケットの取得をシミュレートするプロセスです。合計10個のチケットがあり、チケットを取得するために2つのスレッドが開かれます。
チケットは共有リソースであり、2つのスレッドによって消費されるため、スレッドセーフを確保するために、synchronizedキーワードがTicketConsumerのロジックで使用されます。
これは、シンクロナイズドの初心者のときに誰もが書くべき例です。期待される結果は、10枚のチケット、2人がつかむ、各チケットは1人だけがつかむことができます。
しかし、実際の実行結果は次のようになります。最初にログをインターセプトするだけです。
スクリーンショットには3つのボックス部分があります。
一番上の部分は、2人が10枚目のチケットを手に取っていることです。ログ出力からはまったく問題ありません。結局、1人だけがチケットを手に入れて、9枚目のチケットを競うプロセスに入ります。
しかし、以下に示す9番目のチケットの競争は、少し混乱しています。
why抢到第9张票,成功锁到的对象:288246497
mx抢到第9张票,成功锁到的对象:288246497
なぜ両方とも9番目のチケットを取得し、正常にロックされたオブジェクトは同じですか?
このことは認識を超えています。
これらの2つのスレッドが同じロックを取得して、ビジネスロジックを実行するにはどうすればよいでしょうか。
それで、質問者の質問が浮かび上がります。
- 1.同期が有効にならないのはなぜですか?
- 2.ロックオブジェクトSystem.identityHashCodeの出力が同じなのはなぜですか。
なぜうまくいかなかったのですか?
最初に質問を見てみましょう。
まず、ログ出力から、2回目のラウンドで9番目のチケットが取得されたときに同期が失敗することがすでに非常に明確にわかっています。
理論的な知識に裏付けられて、同期が失敗した場合、ロックの問題があるはずです。
ロックが1つだけで、複数のスレッドが同じロックをめぐって競合している場合、同期に問題はありません。
ただし、2つのスレッドは相互排除条件を達成していません。つまり、ここには間違いなく複数のロックがあります。
これは、理論的な知識から推測できる結論です。
結論が最初に導き出されますが、「複数のロックがある」ことをどのように証明できますか?
同期に入ることができるということは、ロックを取得する必要があることを意味するので、各スレッドによって保持されているロックが何であるかを確認するだけで済みます。
では、スレッドが保持しているロックを確認するにはどうすればよいでしょうか。
jstackコマンド、スレッドスタック関数の出力、わかりますか?
この情報はスレッドスタックに隠されており、取り出すと確認できます。
スレッドスタックを理解する方法は?
これは、アイデアをデバッグするためのちょっとしたトリックです。これは、以前の記事で何度も登場したはずです。
まず、スレッドスタック情報を取得するために、ここでスリープ時間を10秒に調整しました。
実行後、ここで「カメラ」アイコンをクリックします。
数回クリックすると、クリックの時点に対応するいくつかのダンプ情報が表示されます。
最初の2つのロックを監視する必要があり、スレッドがロックに入るたびに10秒間待機するため、プロジェクトの起動の最初の10と次の10の間に1回クリックするだけです。
データをより直感的に観察するために、次のアイコンをクリックしてダンプ情報をコピーすることにしました。
コピーされた情報はたくさんありますが、気にする必要があるのは、whyとmxの2つのスレッドだけです。
これは、最初のダンプの途中にある関連情報です。
mxスレッドはBLOCKED状態にあり、アドレス0x000000076c07b058でロックを待機しています。
スレッドがTIMED_WAITING状態にある理由はスリープ状態であり、ロックを取得してビジネスロジックを実行していることを示しています。そして、それが取得するロックは、偶然にも、mxスレッドが待機しているのは0x000000076c07b058です。
出力ログから判断すると、最初のチケットグラブは、whyスレッドによって実際にグラブされました。
ダンプの情報から、2つのスレッドが同じロックを争っているので、初めて問題はありません。
では、2番目のダンプ情報を見てみましょう。
今回は、両方のスレッドがTIMED_WAITINGにあり、両方ともスリープしています。これは、ロックを取得してビジネスロジックに入ったことを示しています。
しかし、よく見ると、2つのスレッドによって保持されているロックは同じロックではないことがわかります。
mxロックは0x000000076c07b058です。
ロックが0x000000076c07b048である理由。
同じロックではないため、競合関係はなく、両方が同期してビジネスロジックを実行できるため、両方のスレッドがスリープ状態になり、問題はありません。
次に、2つのダンプの情報をまとめて、より直感的にわかるようにします。
0x000000076c07b058を「ロック1」に置き換え、0x000000076c07b048を「ロック2」に置き換えた場合。
その場合、プロセスは次のようになります。
ロックが成功すると、ビジネスロジックが実行され、mxがロック待機状態になります。
ロック1を解放する理由、ロック1のmxがウェイクアップするのを待ち、ロック1を保持して、ビジネスを続行します。
同時に、2番目のロックが成功し、ビジネスロジックが実行される理由。
スレッドスタックから、同期が有効にならなかった理由はロックが変更されたためであることを証明しました。
同時に、スレッドスタックから、ロックオブジェクトSystem.identityHashCodeの出力が同じである理由もわかります。
最初のダンプ中、チケットはすべて10であり、mxはロックを取得せず、同期によってロックされました。
スレッドがticket--操作を実行する理由、チケットは9になりますが、mxスレッドによってロックされたモニターはまだticket = 10のオブジェクトであり、チケットが次のように変更されたためではなく、モニターの_EntryListで待機しています。変化する。
したがって、whyスレッドがロックを解放すると、mxスレッドはロックを取得して実行を継続し、そのticket=9を検出します。
また、なぜ新しいロックを取得し、同期ロジックに入ることができ、ticket=9であることがわかりました。
いいやつ、チケットはすべて9です。System.identityHashCodeは違うのでしょうか?
ロック1を解放した後、なぜロック1を求めてmxと競合し続ける必要があるのかは当然ですが、新しいロックをどこで取得したかはわかりません。
次に、疑問が生じます。なぜロックが変更されたのですか?
誰が私の錠を動かしたのですか?
前回の分析の結果、ロックが実際に変更されていることを確認しました。これを分析すると、激怒し、テーブルを叩き、「どのメロンが私のロックを動かしたのですか?」と叫びました。これはたわごとではないですか?
私の経験によると、この時点で急いでいるのではなく、見下ろし続けると、ピエロは実際にはあなた自身であることがわかります。
チケットを取得した後、チケットの操作が実行されますが、このチケットはロックの対象ではありませんか?
この時、あなたは太ももを叩き、突然気づき、見物人に「大した問題ではなく、ただ握手しているだけだ」と言いました。
だから私は手を振って、ロックされた場所をこれに変更しました:
synchronized (TicketConsumer.class)
クラスオブジェクトをロックオブジェクトとして使用すると、ロックの一意性が保証されます。
何も問題はなく、完璧であり、作業は終了していることが確認されています。
しかし、それは本当に終わったのでしょうか?
実際、ロックオブジェクトが変更された理由については、まだ言われていないことが少しあります。
それはバイトコードに隠されています。
javapコマンドを使用してバイトコードを確認でき、次の情報を確認できます。
Integer.valueOfこれは何ですか?
-128から127までの整数のよく知られたキャッシュ。
つまり、このプログラムでは、開梱とパッキングのプロセスが含まれ、このプロセス中にInteger.valueOfメソッドが呼び出されます。具体的には、チケットの操作です-。
整数の場合、値がキャッシュ範囲内にあるときに同じオブジェクトが返されます。キャッシュ範囲を超えると、毎回新しいオブジェクトが作成されます。
これはバグウェンの必携の知識ポイントであるはずですが、ここでこれを強調して何を表現したいですか?
非常に簡単です。理解できるようにコードを変更するだけです。
初期投票数を10から200に変更し、キャッシュ範囲を超えました。プログラムの結果は次のとおりです。
明らかに、最初のログ出力から、ロックは同じロックではありません。
これは私が前に言ったことです:キャッシュ範囲を超えているため、new Integer(200)の操作は2回実行されます。これらは2つの異なるオブジェクトです。ロックとして使用される場合、これらは2つの異なるロックです。
10に戻し、1回実行すると、次のように感じられます。
ログ出力から、現時点ではロックは1つしかないため、チケットを取得するスレッドは1つだけです。
10はキャッシュ範囲内の数値であるため、毎回キャッシュから取得される同じオブジェクトです。
この短い段落を書く目的は、整数がキャッシュを持っているという知識のポイントを反映することです、誰もがそれを知っています。ただし、他のものと混合する場合は、このキャッシュによって引き起こされる問題を分析する必要があります。これは、乾燥した知識ポイントを直接記憶するよりも効果的です。
しかし...
最初のチケットは10で、チケットは後でチケットが9になり、これもキャッシュ範囲内にあります。ロックが変更されたのはなぜですか。
この質問がある場合は、もう一度考えてみることをお勧めします。
10は10、9は9です。
これらはすべてキャッシュ範囲内にありますが、元々は2つの異なるオブジェクトであり、キャッシュを構築するときにも新しくなります。
なぜ私はこのばかげた見た目のステートメントを追加するのですか?
インターネットで他の同様の質問を見ると、一部の記事が明確に書かれていないため、読者は「キャッシュ範囲の値はすべて同じオブジェクトである」と誤解し、初心者を誤解させる可能性があります。
一言で 言えば、整数をロックオブジェクトとして使用しないでください。それを把握することはできません。
しかし...
スタックオーバーフロー
ただし、記事を書いたときに、stackoverflowについて同様の質問がありました。
この人の問題は次のとおりです。彼は整数をロックオブジェクトとして使用できないことを知っていますが、彼の要件は整数をロックオブジェクトとして使用する必要があるようです。
https://stackoverflow.com/questions/659915/synchronizing-on-an-integer-value
彼の問題をあなたに説明します。
まず、①というラベルの付いた場所を見てください。彼のプログラムは実際には最初にキャッシュから取得されます。キャッシュがない場合は、データベースから取得されてから、キャッシュに配置されます。
非常にシンプルで明確なロジック。
しかし、同じIDを同時に取得するスレッドが複数あるが、このIDに対応するデータがキャッシュにない場合、これらのスレッドはデータベースにクエリを実行してキャッシュを維持するアクションを実行するという同時シナリオを検討します。 。
クエリとストレージのアクションに対応して、彼はそれを説明するために「かなり高価」という用語を使用しました。
「かなり高い」という意味ですが、率直に言って、このアクションは非常に「重い」ので、繰り返さない方がいいでしょう。
したがって、1つのスレッドにかなりコストのかかる操作を実行させるだけです。
そこで彼は②と記された場所のコードを考えました。
同期を使用してIDをロックします。残念ながら、IDのタイプは整数です。
③というラベルの付いた場所で、彼はそれを自分で言いました。異なる整数オブジェクトはロックを共有しないので、同期は役に立ちません。
実際、彼の文章は厳密ではありません。前の分析の後、キャッシュ範囲内のIntegerオブジェクトは引き続き同じロックを共有することがわかります。ここでの「共有」は、競合を意味します。
しかし、明らかに、彼のID範囲は整数キャッシュ範囲よりも大きくなければなりません。
それで、疑問が生じます:このことをどうするか?
この質問を見て最初に頭に浮かんだ質問は、上記の要件を頻繁に実行しているようですが、どのように実行したのでしょうか。
数秒間考えてみたところ、突然、すべて分散アプリケーションになり、Redisを直接ロックに使用していることに気付きました。
まったく考えたことはありません。
現在Redisが許可されていない場合、それは単一のアプリケーションですが、それを解決するにはどうすればよいですか?
高い評価の答えを見る前に、この質問の下にあるコメントを見てみましょう。
最初の3文字:参考までに。
それがポイントではないので、あなたがそれを理解していなくても構いません。
でも、私の英語はとても高いので、ちなみに英語を教えます。
参考までに、一般的に使用される英語の略語であり、フルネームは参考のために情報を提供するためのものです。
つまり、彼は後であなたのために文書を添付したに違いありません。翻訳は次のとおりです。BrianGoetzは、Devoxx 2018のスピーチで、整数をロックとして使用してはならないと述べました。
このリンクから説明のこの部分に直接アクセスできます。リスニングの練習は30秒未満です:
https ://www.youtube.com/watch?v = 4r2Wg-TY7gU&t = 3289s
それで、質問は再び来ますか?
ブライアン・ゲッツとは誰ですか、そしてなぜ彼は権威があるように聞こえますか?
Java言語の開発者であるOracleのJavaLanguageArchitectは、恐れているかどうか尋ねます。
同時に、彼は私が何度も推薦した「Java並行プログラミングの実践」という本の著者でもあります。
さて、私は大物の支持を見つけたので、Gaozanが答えで言ったことをお見せします。
前の部分では詳しく説明しませんが、実際には、前述のポイントです。整数は使用できません。内部キャッシュと外部キャッシュが関係します。
下線部分に注意してください、私はあなたのためにそれを翻訳するために私自身の理解を追加します:
整数をロックとして実際に使用する必要がある場合は、マップまたは整数のセットを作成する必要があります。コレクションクラスでマッピングを行うことにより、マッピングが必要なものの明確なインスタンスであることを確認できます。そして、このインスタンスはロックとして使用できます。
次に、彼はこのコードスニペットを提供します。
ConcurrentHashMapを使用してから、putIfAbsentメソッドを使用してマッピングを実行します。
たとえば、locks.putIfAbsent(200、200)が複数回呼び出された場合、マップには値が200の整数オブジェクトが1つだけあります。これはマップの特性によって保証されており、あまり説明する必要はありません。 。
でもこの相棒はとてもいいです。誰かがこの角を曲がることができないのを防ぐために、彼はもう一度みんなにそれを説明しました。
まず、彼はあなたが書くこともできると言います:
ただし、この方法では、少額のコストが発生します。つまり、アクセスするたびに、値がマップされていない場合は、オブジェクトオブジェクトが作成されます。
これを回避するために、彼は整数自体をマップに保持するだけです。これを行う目的は何ですか?これは、整数自体を直接使用することとどう違うのですか?
彼はそれをこのように説明しました、それは実際に私が以前に言ったことです「これは地図の特徴が保証するものです」:
マップからget()を実行する場合、equals()メソッドを使用してキーを比較します。
equals()メソッドを呼び出す同じ値の2つの異なる整数インスタンスは同じであると判断されます。
したがって、getCacheSyncObjectへの引数として「newInteger(5)」のさまざまな整数インスタンスをいくつでも渡すことができますが、常にその値を含む最初に渡されたインスタンスのみを取得します。
それが私の言いたいことです:
文を要約すると、マップを介してマップされます。新しい整数の数に関係なく、これらの整数は同じ整数にマップされるため、整数キャッシュを超えた場合でも、ロックは1つだけになります。
高い評価の答えに加えて、私が言いたい他の2つの答えがあります。
最初はこれです:
彼の言ったことは気にしないでください、しかし私がこの文の翻訳を見たとき私はショックを受けました:
この猫の皮???
それはとても残酷です。
当時、この翻訳は正しくないはずだと思っていました。少し俗語に違いありません。それで私はそれをチェックアウトしました、そしてそれはこれであることがわかりました:
英語の知識を少し無料でお送りします。どういたしまして。
懸念すべき2番目の答えは最後にあります:
この仲間は、あなたが探している答えを持っている「Java並行プログラミングの実際」のセクション5.6の内容を見るようにあなたに言いました。
偶然にも、この本は手元にあったので、開いて見てみました。
セクション5.6のタイトルは「効率的でスケーラブルな結果キャッシュの構築」です。
男、私はこのセクションを詳しく見て、これが赤ちゃんであることがわかりました。
あなたが読んだ本のサンプルコード:
質問した人のコードと全く同じではないですか?
それらはすべてキャッシュから取得され、使用できない場合は再構築されます。
違いは、本がメソッドに同期を追加することです。しかし、この本は、問題を引き出すためだけに、これが最悪の解決策であるとも述べています。
次に、ConcurrentHashMap、putIfAbsent、FutureTaskを使用して、比較的優れたソリューションを提供しました。
問題が別の角度から解決されていることがわかります。同期に絡み合いはまったくなく、2番目の方法では同期が直接削除されます。