この記事は、Java 同時実行に関する一般的な基本的な面接の質問をすべての人に向けてまとめたものです。これには一定の参考価値があります。必要な友人は参照できます。すべての人に役立つことを願っています。
#1. スレッドとプロセスとは何ですか?
1.1. 内容プロセスとは何ですか?
#プロセスはプログラムの実行プロセスであり、プログラムを実行するシステムの基本単位であるため、プロセスは動的です。システム上でプログラムを実行することは、プロセスの作成、操作、終了までのプロセスです。 Java では、main 関数を開始すると、実際には JVM プロセスが開始されます。main 関数が配置されているスレッドは、このプロセス内のスレッドであり、メイン スレッドとも呼ばれます。 下の図に示すように、Windows のタスク マネージャーを表示すると、ウィンドウ内で現在実行中のプロセス (.exe ファイルの実行) が明確にわかります。1.2. スレッドとは何ですか?
スレッドはプロセスと似ていますが、スレッドは異なります。プロセスよりも小さい実行単位。プロセスは実行中に複数のスレッドを生成できます。プロセスとは異なり、同じタイプの複数のスレッドはプロセスのヒープ リソースと メソッド領域 リソースを共有しますが、各スレッドには独自の プログラム カウンタ、# があります。 ## 仮想マシンスタック と ローカルメソッドスタック なので、システムがスレッドを生成したりスレッドを切り替えたりする際の負荷は、プロセスに比べてはるかに小さくなります。軽量プロセスと呼ばれます。 Java プログラムは本質的にマルチスレッド プログラムです。JMX を使用すると、通常の Java プログラムにどのようなスレッドがあるかを確認できます。コードは次のとおりです。
public class MultiThread { public static void main(String[] args) { // 获取 Java 线程管理 MXBean ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); // 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); // 遍历线程信息,仅打印线程 ID 和线程名称信息 for (ThreadInfo threadInfo : threadInfos) { System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); } } }
上記のプログラムの出力は次のとおりです (出力内容は異なる場合があります。以下の各スレッドの役割についてはあまり心配しないでください。メインスレッドが main メソッドを実行することだけを理解してください)。
[5] Attach Listener //添加事件 [4] Signal Dispatcher // 分发处理给 JVM 信号的线程 [3] Finalizer //调用对象 finalize 方法的线程 [2] Reference Handler //清除 reference 线程 [1] main //main 线程,程序入口
上記の出力から内容を確認できます。
Java プログラムの実行はメイン スレッドであり、同時に実行される他の複数のスレッドです。2. スレッドとプロセスの関係、違い、利点と欠点について簡単に説明してください。
JVM の観点から見たプロセスとスレッドの関係
2.1. プロセスとスレッドの関係の図解スレッド
次の図は Java メモリ領域です。次の図を通して、JVM の観点からスレッドとプロセスの関係について説明します。 Java メモリ領域 (ランタイム データ領域) についてあまり詳しくない場合は、次の記事を読むことができます: 「Java メモリ領域について説明しているおそらく最も明確な記事」
上の図からわかるように、プロセス内には複数のスレッドが存在する可能性があり、複数のスレッドはプロセス ## の
ヒープ# を共有し、
リソースですが、各スレッドには独自の プログラム カウンター 、 仮想マシン スタック 、および ローカル メソッド スタック## #。 概要: スレッドは、より小さな実行単位に分割されたプロセスです。スレッドとプロセスの最大の違いは、各プロセスは基本的に独立しているが、同じプロセス内のスレッドは相互に影響を与える可能性が非常に高いため、必ずしも各スレッドが独立しているわけではないことです。スレッド実行のオーバーヘッドは小さいですが、リソースの管理や保護には役に立ちません。逆に、プロセス
以下は、このナレッジ ポイントを拡張したものです。この質問について考えてみましょう: プログラム カウンタ
、仮想マシン スタック
、およびローカル メソッド スタックはなぜスレッドに対してプライベートなのでしょうか?ヒープとメソッド領域がスレッドによって共有されるのはなぜですか? #2.2. プログラム カウンタが非公開なのはなぜですか?
#プログラム カウンタには主に次の 2 つの機能があります:バイトコード インタプリタは、プログラム カウンターを変更することで命令を順番に読み取り、逐次実行、選択、ループ、例外処理などのコード フロー制御を実現します。
マルチスレッドの場合、プログラム カウンタは現在のスレッドの実行位置を記録するために使用されます。これにより、スレッドが切り替えられたときに、スレッドが最後にどこで実行されたかを知ることができます。
したがって、スレッド内のローカル変数が他のスレッドからアクセスされないように するために、仮想マシン スタックとローカル メソッド スタックはスレッド プライベートになります。
2.4. ヒープとメソッド領域を 1 文で簡単に理解する
ヒープとメソッド領域は、すべてのスレッドで共有されるリソースです。ヒープは、メモリの最大部分は主に新しく作成されたオブジェクトを保存するために使用されます (すべてのオブジェクトはここでメモリが割り当てられます)。メソッド領域は主に、ロードされたクラス情報、定数、静的変数、直前にコンパイルされたコードを保存するために使用されます。インタイムコンパイラとその他のデータ。3. 同時実行と並列処理の違いについて話しますか?
#4. マルチスレッドを使用する理由は何ですか?#一般的に話しましょう:
5. マルチスレッドを使用するとどのような問題が発生する可能性がありますか? 同時プログラミングの目的は、実行を改善することです。プログラムの効率 プログラムの実行速度が向上しますが、同時プログラミングによって常にプログラムの実行速度が向上するとは限らず、同時プログラミングでは、メモリ リーク、コンテキスト スイッチ、デッドロック、ハードウェアとソフトウェアによるアイドル リソースの制限など、多くの問題が発生する可能性があります。
6. スレッドのライフサイクルとステータスについて話しますか?Java スレッドは、指定された瞬間にのみ以下に存在できます。実行ライフサイクル 6 つの異なる状態の 1 つ (画像ソース「The Art of Java Concurrent Programming」セクション 4.1.4)。
#スレッドはライフサイクル中に特定の状態に固定されるのではなく、コードが実行されるとさまざまな状態の間で切り替わります。 Java スレッドの状態変化を以下の図に示します (画像ソース「The Art of Java Concurrent Programming」セクション 4.1.4)。上図: スレッドが作成された後、スレッドは
NEW (新しい)状態で、
start()メソッドを呼び出した後に実行を開始します。この時点では READY (実行可能) 状態です。実行可能状態のスレッドは、CPU タイム スライス (タイムスライス) を取得した後、
RUNNING 状態になります。 当线程执行 7. 什么是上下文切换? 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。 概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 8. 什么是线程死锁?如何避免死锁? 8.1. 认识线程死锁 多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。 下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》): Output 线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过 学过操作系统的朋友都知道产生死锁必须具备以下四个条件: 8.2. 如何避免线程死锁? 我们只要破坏产生死锁的四个条件中的其中一个就可以了。 破坏互斥条件 这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。 破坏请求与保持条件 一次性申请所有的资源。 破坏不剥夺条件 占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 破坏循环等待条件 靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 我们对线程 2 的代码修改成下面这样就不会产生死锁了。 Output 我们分析一下上面的代码为什么避免了死锁的发生? 线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。 9. 说说 sleep() 方法和 wait() 方法区别和共同点? 10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法? 这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来! new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。 总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。 推荐教程:java教程操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJava:Java Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。
wait()
方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)
方法或 wait(long millis)
方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的 run()
方法之后将会进入到 TERMINATED(终止) 状态。public class DeadLockDemo {
private static Object resource1 = new Object();//资源 1
private static Object resource2 = new Object();//资源 2
public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 1").start();
new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "线程 2").start();
}
}
Thread[线程 1,5,main]get resource1
Thread[线程 2,5,main]get resource2
Thread[线程 1,5,main]waiting get resource2
Thread[线程 2,5,main]waiting get resource1
Thread.sleep(1000);
让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "线程 2").start();
Thread[线程 1,5,main]get resource1
Thread[线程 1,5,main]waiting get resource2
Thread[线程 1,5,main]get resource2
Thread[线程 2,5,main]get resource1
Thread[线程 2,5,main]waiting get resource2
Thread[线程 2,5,main]get resource2
Process finished with exit code 0
以上がJava 同時実行の基本に関するよくある面接の質問 (概要)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。