Java 同時実行の基本に関するよくある面接の質問 (概要)

青灯夜游
リリース: 2019-11-23 16:37:15
転載
2294 人が閲覧しました

この記事は、Java 同時実行に関する一般的な基本的な面接の質問をすべての人に向けてまとめたものです。これには一定の参考価値があります。必要な友人は参照できます。すべての人に役立つことを願っています。

Java 同時実行の基本に関するよくある面接の質問 (概要)

#1. スレッドとプロセスとは何ですか?

1.1. 内容プロセスとは何ですか?

#プロセスはプログラムの実行プロセスであり、プログラムを実行するシステムの基本単位であるため、プロセスは動的です。システム上でプログラムを実行することは、プロセスの作成、操作、終了までのプロセスです。

Java では、main 関数を開始すると、実際には JVM プロセスが開始されます。main 関数が配置されているスレッドは、このプロセス内のスレッドであり、メイン スレッドとも呼ばれます。

下の図に示すように、Windows のタスク マネージャーを表示すると、ウィンドウ内で現在実行中のプロセス (.exe ファイルの実行) が明確にわかります。

Java 同時実行の基本に関するよくある面接の質問 (概要)

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 メモリ領域について説明しているおそらく最も明確な記事」

Java 同時実行の基本に関するよくある面接の質問 (概要)


上の図からわかるように、プロセス内には複数のスレッドが存在する可能性があり、複数のスレッドはプロセス ## の
ヒープ# を共有し、

メソッド領域 (JDK1.8 以降のメタスペース)

リソースですが、各スレッドには独自の プログラム カウンター 仮想マシン スタック 、および ローカル メソッド スタック## #。 概要: スレッドは、より小さな実行単位に分割されたプロセスです。スレッドとプロセスの最大の違いは、各プロセスは基本的に独立しているが、同じプロセス内のスレッドは相互に影響を与える可能性が非常に高いため、必ずしも各スレッドが独立しているわけではないことです。スレッド実行のオーバーヘッドは小さいですが、リソースの管理や保護には役に立ちません。逆に、プロセス

以下は、このナレッジ ポイントを拡張したものです。

この質問について考えてみましょう: プログラム カウンタ

仮想マシン スタック

、および

ローカル メソッド スタックはなぜスレッドに対してプライベートなのでしょうか?ヒープとメソッド領域がスレッドによって共有されるのはなぜですか? #2.2. プログラム カウンタが非公開なのはなぜですか?

#プログラム カウンタには主に次の 2 つの機能があります:バイトコード インタプリタは、プログラム カウンターを変更することで命令を順番に読み取り、逐次実行、選択、ループ、例外処理などのコード フロー制御を実現します。

マルチスレッドの場合、プログラム カウンタは現在のスレッドの実行位置を記録するために使用されます。これにより、スレッドが切り替えられたときに、スレッドが最後にどこで実行されたかを知ることができます。

    ネイティブメソッド実行時はプログラムカウンタに不定のアドレスが記録されますが、Javaコード実行時のみプログラムカウンタに次の命令のアドレスが記録されます。
  1. したがって、プログラム カウンターを非公開にしておく目的は、主に
  2. スレッドを切り替えた後にプログラム カウンターを正しい実行位置に復元することです。

2.3. 仮想マシン スタックとローカル メソッド スタックがプライベートなのはなぜですか?

  • 仮想マシン スタック: 各 Java メソッドが実行されると、ローカル変数テーブル、オペランド スタック、定数プール参照などの情報を格納するスタック フレームが作成されます。メソッドの呼び出しから実行完了までの処理は、Java 仮想マシンのスタックにスタック フレームをプッシュおよびポップする処理に相当します。
  • ローカル メソッド スタック: 仮想マシン スタックが果たす役割は非常に似ていますが、相違点は次のとおりです。 仮想マシン スタックは、仮想マシンに Java メソッド (つまり、 、バイトコード)、ローカル メソッド スタックは、仮想マシンによって使用されるネイティブ メソッドを提供します。 HotSpot 仮想マシン内の Java 仮想マシン スタックと結合されます。

したがって、スレッド内のローカル変数が他のスレッドからアクセスされないように するために、仮想マシン スタックとローカル メソッド スタックはスレッド プライベートになります。

2.4. ヒープとメソッド領域を 1 文で簡単に理解する

ヒープとメソッド領域は、すべてのスレッドで共有されるリソースです。ヒープは、メモリの最大部分は主に新しく作成されたオブジェクトを保存するために使用されます (すべてのオブジェクトはここでメモリが割り当てられます)。メソッド領域は主に、ロードされたクラス情報、定数、静的変数、直前にコンパイルされたコードを保存するために使用されます。インタイムコンパイラとその他のデータ。

3. 同時実行と並列処理の違いについて話しますか?

  • 同時実行: 同時にperiod, multiple すべてのタスクが実行されています (単位時間あたり同時に実行されるとは限りません);
  • Parallel: 単位時間あたり複数のタスクが同時に実行されます。

#4. マルチスレッドを使用する理由は何ですか?#一般的に話しましょう:

    コンピュータの底から:
  • スレッドは、プログラム実行の最小単位である軽量プロセスにたとえることができ、スレッド間の切り替えとスケジューリングのコストは、プロセスのコストよりもはるかに低くなります。さらに、マルチコア CPU の時代では、複数のスレッドを同時に実行できるため、スレッド コンテキストの切り替えのオーバーヘッドが軽減されます。
  • 現代のインターネット開発トレンドの観点から:
  • 今日のシステムでは、多くの場合、数百万、さらには数千万の同時実行性が必要であり、マルチスレッド同時プログラミングは、同時実行性の高いシステムを開発するための基礎です。 、マルチスレッド メカニズムを使用すると、システム全体の同時実行性とパフォーマンスが大幅に向上します。
  • コンピューターの最下層をさらに深く見てみましょう:

    シングルコア時代:
  • シングルコア時代では、マルチスレッドCPUとIO機器の総合稼働率の向上が主な目的でした。たとえば、スレッドが 1 つだけの場合、CPU が計算を実行しているときは IO デバイスはアイドル状態になり、IO 操作を実行しているときは CPU はアイドル状態になります。現時点では両者の稼働率は50%程度と単純に言えます。しかし、スレッドが 2 つある場合は異なり、1 つのスレッドが CPU 計算を実行すると、もう 1 つのスレッドが IO 操作を実行できるため、理想的な状況では 2 つのスレッドの使用率が 100% に達します。
  • マルチコア時代:
  • マルチコア時代のマルチスレッドは、主に CPU 使用率を向上させることを目的としています。たとえば、複雑なタスクを計算する場合、スレッドを 1 つだけ使用すると、CPU の 1 つの CPU コアのみが使用されますが、複数のスレッドを作成すると、複数の CPU コアを使用できるようになり、CPU 使用率のパフォーマンスが向上します。

5. マルチスレッドを使用するとどのような問題が発生する可能性がありますか? 同時プログラミングの目的は、実行を改善することです。プログラムの効率 プログラムの実行速度が向上しますが、同時プログラミングによって常にプログラムの実行速度が向上するとは限らず、同時プログラミングでは、メモリ リーク、コンテキスト スイッチ、デッドロック、ハードウェアとソフトウェアによるアイドル リソースの制限など、多くの問題が発生する可能性があります。

6. スレッドのライフサイクルとステータスについて話しますか?Java スレッドは、指定された瞬間にのみ以下に存在できます。実行ライフサイクル 6 つの異なる状態の 1 つ (画像ソース「The Art of Java Concurrent Programming」セクション 4.1.4)。

Java 同時実行の基本に関するよくある面接の質問 (概要)#スレッドはライフサイクル中に特定の状態に固定されるのではなく、コードが実行されるとさまざまな状態の間で切り替わります。 Java スレッドの状態変化を以下の図に示します (画像ソース「The Art of Java Concurrent Programming」セクション 4.1.4)。上図: スレッドが作成された後、スレッドは

NEW (新しい)

状態で、

start()

メソッドを呼び出した後に実行を開始します。この時点では READY (実行可能) Java 同時実行の基本に関するよくある面接の質問 (概要) 状態です。実行可能状態のスレッドは、CPU タイム スライス (タイムスライス) を取得した後、

RUNNING

状態になります。

操作系统隐藏 Java 虚拟机(JVM)中的 RUNNABLE 和 RUNNING 状态,它只能看到 RUNNABLE 状态(图源:HowToDoInJavaJava Thread Life Cycle and Thread States),所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。

Java 同時実行の基本に関するよくある面接の質問 (概要)

当线程执行 wait()方法之后,线程进入 WAITING(等待) 状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而 TIME_WAITING(超时等待) 状态相当于在等待状态的基础上增加了超时限制,比如通过 sleep(long millis)方法或 wait(long millis)方法可以将 Java 线程置于 TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的 run() 方法之后将会进入到 TERMINATED(终止) 状态。

7. 什么是上下文切换?

多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。

概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换

上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。

Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。

8. 什么是线程死锁?如何避免死锁?

8.1. 认识线程死锁

多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

Java 同時実行の基本に関するよくある面接の質問 (概要)

下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):

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();
    }
}
ログイン後にコピー

Output

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
ログイン後にコピー

线程 A 通过 synchronized (resource1) 获得 resource1 的监视器锁,然后通过 Thread.sleep(1000);让线程 A 休眠 1s 为的是让线程 B 得到执行然后获取到 resource2 的监视器锁。线程 A 和线程 B 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。

学过操作系统的朋友都知道产生死锁必须具备以下四个条件:

  1. 互斥条件:该资源任意一个时刻只由一个线程占用。
  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

8.2. 如何避免线程死锁?

我们只要破坏产生死锁的四个条件中的其中一个就可以了。

破坏互斥条件

这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

破坏请求与保持条件

一次性申请所有的资源。

破坏不剥夺条件

占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

破坏循环等待条件

靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

我们对线程 2 的代码修改成下面这样就不会产生死锁了。

        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();
ログイン後にコピー

Output

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
ログイン後にコピー

我们分析一下上面的代码为什么避免了死锁的发生?

线程 1 首先获得到 resource1 的监视器锁,这时候线程 2 就获取不到了。然后线程 1 再去获取 resource2 的监视器锁,可以获取到。然后线程 1 释放了对 resource1、resource2 的监视器锁的占用,线程 2 获取到就可以执行了。这样就破坏了破坏循环等待条件,因此避免了死锁。

9. 说说 sleep() 方法和 wait() 方法区别和共同点?

  • 两者最主要的区别在于:sleep 方法没有释放锁,而 wait 方法释放了锁
  • 两者都可以暂停线程的执行。
  • Wait 通常被用于线程间交互/通信,sleep 通常被用于暂停执行。
  • wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用wait(long timeout)超时后线程会自动苏醒。

10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!

new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。

总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

推荐教程:java教程

以上がJava 同時実行の基本に関するよくある面接の質問 (概要)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:segmentfault.com
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート