今日は、Javaのマルチスレッド化されたインタビューの質問を共有します。インタビューは数年前に試されたときに尋ねられました。そのとき、それを書く方法がわかりませんでした。結局、私はまだ運用と保守を続けていました。今私は他の人にインタビューするときに時々この質問をします。具体的なトピックは次のとおりです。2つのスレッドが交互に1から100を順番に出力します。最初のスレッドは偶数のみを出力でき、2番目のスレッドは奇数を出力します。2つの子が順番に番号を呼び出すと想像してください。
マルチスレッドプログラミングの基礎がない場合、確かにこの質問の方法はわかりませんが、マルチスレッドプログラミングを少し習得した人にとってはそれほど難しくないと思われるので、並行性があるかどうかに関係なく、これは良いカードだと思いますプログラミング経験に関する質問。さらに、この質問から多くの知識ポイントを拡張できるため、中級および上級エンジニアへのインタビューでよくある質問になっています。この質問には多くの解決策があります。次に、単純なものから複雑なものへと進み、徐々に理解していきましょう。最後に、この質問についていくつかの拡張された質問も行います。
2つのスレッドが交互に出力するため、2つのスレッドが連携する必要があります。連携とは、2つのスレッド間で情報を転送する必要があることを意味します。相互に情報を転送する方法は?0〜100の数字が交互に順番に出力されるため、各プロセスは時々カウンターの値を確認し、それが出力の順番であるかどうかを確認するだけでよいと直接考えるかもしれません。はい、これはソリューション1のアイデアです。
解決策1
上記のアイデアを使えば、次のコードを間違いなくすばやく書くことができます。
public class PrintNumber extends Thread {
private static int cnt = 0;
private int id; // 线程编号
public PrintNumber(int id) {
this.id = id;
}
@Override
public void run() {
while (cnt < 100) {
while (cnt%2 == id) {
cnt++;
System.out.println("thread_" + id + " num:" + cnt);
}
}
}
public static void main(String[] args) {
Thread thread0 = new PrintNumber(0);
Thread thread1 = new PrintNumber(1);
thread0.start();
thread1.start();
}
}
しかし、実際に実行すると、あなたはそれを見つけるでしょう!!!
thread_0 num:1
thread_0 num:3
thread_1 num:3
thread_1 num:5
thread_1 num:6
thread_0 num:5
thread_0 num:8
thread_0 num:9
thread_1 num:8
thread_0 num:11
thread_1 num:11
.........
順序が間違っているだけでなく、重複や損失もあります!問題はどこだ?cnt++; System.out.println("thread_" + id + " num:" + cnt);
この2行のコードに戻ると、主に2つのアクションcnt++
と出力が含まれていますcnt ++が実行されると、別のスレッドの出力がトリガーされた可能性があります。実行プロセスを簡素化します。JVMには、瞬間的に実行される4つのアクションがあります。
- thread_0 cnt ++
- thread_0印刷
- thread_1 cnt ++
- thread_1 print
Java as-if-serial semanticsによると 、jvmは単一のスレッド内の順序のみを保証し、複数のスレッド間の順序は保証しないため、上記の4つのステップの実行順序は1 2 3 4または可能性があります。 1 3 2 4、より可能性の高い1 3 4 2です。上記のコードでは、最終的な順序が変わる可能性があります。さらに、cnt ++は、2行の低レベル命令に分解できますtmp = cnt + 1; cnt = tmp
。2つのスレッドが上記の命令を同時に実行すると、1 2 3 4ステップと同じ問題に直面します。そうです、複数のスレッドでの動作とガールフレンド彼の心も同様にとらえどころのないです。この問題を解決するには?正しいソリューション1と以下のソリューションは基本的に同じ原理ですが、実装が異なります。
ソリューション1の正しいコードは次のとおりです。
public class PrintNumber extends Thread {
private static AtomicInteger cnt = new AtomicInteger();
private int id;
public PrintNumber(int id) {
this.id = id;
}
@Override
public void run() {
while (cnt.get() <= 100) {
while (cnt.get()%2 == id) {
System.out.println("thread_" + id + " num:" + cnt.get());
cnt.incrementAndGet();
}
}
}
public static void main(String[] args) {
Thread thread0 = new PrintNumber(0);
Thread thread1 = new PrintNumber(1);
thread0.start();
thread1.start();
}
}
上記のコードは、cnt ++の操作をAtomicIntegerのincrementAndGetメソッドを介してアトミック操作に変え、同時にcntを操作する複数のスレッドによって引き起こされるデータエラーを回避します。さらに、while (cnt.get()%2 == id
実行のためにwhileループに入ることができるスレッドが1つだけで、現在のスレッドのみが実行されるようにすることもできます。 incの後、次のスレッドが印刷を実行できるため、このコードは代替の出力ニーズを満たすことができます。ただし、この方法は制御が難しいため、run関数を次のように記述します。
@Override
public void run() {
while (cnt.get() <= 100) {
while (cnt.get()%2 == id) {
cnt.incrementAndGet();
System.out.println("thread_" + id + " num:" + cnt.get());
}
}
}
printとcnt.incrementAndGet()の位置を変更するだけで、結果は完全に異なります。最初に、incは、printが実行される前に次のスレッドが実行に入り、cntの値を変更する可能性があり、誤った結果をもたらします。また、この方法は実際には厳密には正しくありません。印刷されないが他の同様のシナリオの場合、問題が発生する可能性があるため、この書き込み方法は強くお勧めしません。
ソリューション2
実際、cnt ++とprintで同時に実行するスレッドは1つだけなので、方法1で間違ったソリューションに同期を追加するだけで済みます。コードは次のとおりです。
public class PrintNumber extends Thread {
private static int cnt = 0;
private int id; // 线程编号
public PrintNumber(int id) {
this.id = id;
}
@Override
public void run() {
while (cnt <= 100) {
while (cnt%2 == id) {
synchronized (PrintNumber.class) {
cnt++;
System.out.println("thread_" + id + " num:" + cnt);
}
}
}
}
public static void main(String[] args) {
Thread thread0 = new PrintNumber(0);
Thread thread1 = new PrintNumber(1);
thread0.start();
thread1.start();
}
}
ここでは、synchronizedキーワードを使用してcnt ++をラップし、同期コードブロックに出力して、1つのスレッドのみが実行できるようにしました。cntをvolatileとして宣言する必要があるかどうか尋ねられるかどうかはわかりませんが、同期すると可視性を保証できるため、私の答えは「いいえ」です。
上記のコードwhile (cnt.get()%2 == id)
を使用して、出力する数値がcntであるかどうかを常に判断していることに気づきましたか?これは、2人の子供が順番に数値を報告するようなものです。数値を報告するかどうかを選択します。これは明らかに非効率的です。2人の子供がお互いに通知できますか。一方の子供はレポート後にもう一方に通知してから、自分で休憩します。これにより、明らかに両方の当事者のエネルギー消費量が少なくなります。コードに同様のメカニズムを持たせることができれば、無駄な作業が減り、パフォーマンスが向上します。
これは、Javaの待機および通知メカニズムに依存します。スレッドが作業を完了すると、スレッドは別のスレッドを起動し、それ自体がスリープ状態になるため、各スレッドが待機中でビジーになる必要はありません。コード変換は次のとおりですwhile (cnt.get()%2 == id)
。ここで直接削除しました。
@Override
public void run() {
while (cnt <= 100) {
synchronized (PrintNumber.class) {
cnt++;
System.out.println("thread_" + id + " num:" + cnt);
PrintNumber.class.notify();
try {
PrintNumber.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
ソリューション3
同期を使用できる場所でReentrantLockを使用できるため、ソリューション3とソリューション2は基本的に同じです。つまり、同期をロックに置き換え、待機と通知を条件の信号に置き換えて待機します。変更されたコードは次のとおりです。
public class PrintNumber extends Thread {
private static Lock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private int id;
private static int cnt = 0;
public PrintNumber(int id) {
this.id = id;
}
private static void print(int id) {
}
@Override
public void run() {
while (cnt <= 100) {
lock.lock();
System.out.println("thread_" + id + " num:" + cnt);
cnt++;
condition.signal();
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.unlock();
}
}
public static void main(String[] args) {
Thread thread0 = new PrintNumber(0);
Thread thread1 = new PrintNumber(1);
thread0.start();
thread1.start();
}
}
ここで考えられる解決策は非常に多くあります。他に解決策があるかどうかはわかりませんが、このトピックに関連するマルチスレッドの知識ポイントを理解するのに役立つことを願っています。