Heim  >  Artikel  >  Java  >  Allgemeine Fragen zu Java-Parallelität in Vorstellungsgesprächen (Zusammenfassung)

Allgemeine Fragen zu Java-Parallelität in Vorstellungsgesprächen (Zusammenfassung)

青灯夜游
青灯夜游nach vorne
2019-11-23 16:37:152277Durchsuche

Dieser Artikel fasst allgemeine Interviewfragen zur Java-Parallelität zusammen. Er hat einen gewissen Referenzwert. Ich hoffe, dass er für alle hilfreich ist.

Allgemeine Fragen zu Java-Parallelität in Vorstellungsgesprächen (Zusammenfassung)

1. Was sind Threads und Prozesse?

Was Was ist ein Prozess?

Ein Prozess ist ein Ausführungsprozess eines Programms und stellt die Grundeinheit für das System zur Ausführung von Programmen dar, sodass der Prozess dynamisch ist. Das Ausführen eines Programms auf dem System ist der Prozess von der Erstellung über den Betrieb bis zum Beenden eines Prozesses.

Wenn wir in Java die Hauptfunktion starten, starten wir tatsächlich einen JVM-Prozess, und der Thread, in dem sich die Hauptfunktion befindet, ist ein Thread in diesem Prozess, der auch Hauptthread genannt wird.

Wie in der Abbildung unten gezeigt, können wir beim Betrachten des Task-Managers in Windows deutlich den Prozess sehen, der derzeit im Fenster ausgeführt wird (die Ausführung der .exe-Datei).

Allgemeine Fragen zu Java-Parallelität in Vorstellungsgesprächen (Zusammenfassung)

1.2. Was ist ein Thread?

Ein Thread ähnelt einem Prozess, aber a Thread ist ein anderer Prozess als ein Prozess mit kleineren Ausführungseinheiten. Ein Prozess kann während seiner Ausführung mehrere Threads generieren. Anders als bei einem Prozess teilen sich mehrere Threads desselben Typs die Ressourcen Heap und Methodenbereich des Prozesses, aber jeder Thread hat seinen eigenen Programmzähler, Der virtuelle Maschinenstapel und der lokale Methodenstapel . Wenn das System also einen Thread generiert oder zwischen Threads wechselt, ist die Belastung viel geringer als die des Prozesses. Aus diesem Grund sind es auch Threads Man nennt es einen Lightweight-Prozess.

Java-Programme sind von Natur aus Multithread-Programme. Mit JMX können wir sehen, welche Threads ein gewöhnliches Java-Programm hat.

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());
        }
    }
}

Die Ausgabe des obigen Programms ist wie folgt (der Ausgabeinhalt kann unterschiedlich sein, machen Sie sich nicht zu viele Gedanken über die Rolle der einzelnen Threads unten, wissen Sie nur, dass der Hauptthread die Hauptmethode ausführt):

[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
[3] Finalizer //调用对象 finalize 方法的线程
[2] Reference Handler //清除 reference 线程
[1] main //main 线程,程序入口

Aus der obigen Ausgabe ist der Inhalt ersichtlich: Bei der Ausführung eines Java-Programms laufen der Hauptthread und mehrere andere Threads gleichzeitig .

2. Bitte beschreiben Sie kurz die Beziehung, Unterschiede, Vor- und Nachteile zwischen Threads und Prozessen.

Die Beziehung zwischen Prozessen und Threads aus der Sicht von JVM

2.1 Threads

Das Bild unten zeigt den Java-Speicherbereich. Durch das Bild unten können wir über die Beziehung zwischen Threads und Prozessen aus der Perspektive von JVM sprechen. Wenn Sie nicht viel über den Java-Speicherbereich (Laufzeitdatenbereich) wissen, können Sie diesen Artikel lesen: „Der wahrscheinlich klarste Artikel, der den Java-Speicherbereich erklärt“


Allgemeine Fragen zu Java-Parallelität in Vorstellungsgesprächen (Zusammenfassung)

Wie aus dem obigen Bild ersichtlich ist: Es kann mehrere Threads in einem Prozess geben, und mehrere Threads teilen sich den Heap und Methodenbereich (Metaspace nach JDK1.8) Ressourcen, aber jeder Thread hat seinen eigenen Programmzähler , Virtual Machine Stack und Native Method Stack.

Zusammenfassung: Threads sind Prozesse, die in kleinere laufende Einheiten unterteilt sind. Der größte Unterschied zwischen Threads und Prozessen besteht darin, dass jeder Prozess grundsätzlich unabhängig ist, dies jedoch nicht unbedingt bei jedem Thread der Fall ist, da Threads im selben Prozess sich höchstwahrscheinlich gegenseitig beeinflussen. Die Thread-Ausführung hat einen geringen Overhead, ist jedoch nicht förderlich für die Ressourcenverwaltung und den Ressourcenschutz. Das Gegenteil gilt für Prozesse.

Das Folgende ist eine Erweiterung dieses Wissenspunkts!

Lassen Sie uns über diese Frage nachdenken: Warum sind

Programmzähler, Virtual Machine Stack und Local Method Stack privat für Threads? Warum werden der Heap- und Methodenbereich von Threads gemeinsam genutzt?

2.2. Warum ist der Programmzähler privat?

Der Programmzähler hat hauptsächlich die folgenden zwei Funktionen:

    Der Bytecode-Interpreter liest Anweisungen sequentiell, indem er den Programmzähler ändert, wodurch eine Codeflusskontrolle wie sequentielle Ausführung, Auswahl, Schleife und Ausnahmebehandlung realisiert wird.
  1. Bei Multithreading wird der Programmzähler verwendet, um die Ausführungsposition des aktuellen Threads aufzuzeichnen, sodass beim Zurückschalten des Threads bekannt ist, wo der Thread zuletzt ausgeführt wurde.
Es ist zu beachten, dass der Programmzähler bei Ausführung einer nativen Methode eine undefinierte Adresse aufzeichnet. Nur wenn Java-Code ausgeführt wird, zeichnet der Programmzähler die Adresse der nächsten Anweisung auf.

Daher besteht der Hauptzweck, den Programmzähler privat zu halten, darin,

ihn nach dem Thread-Wechsel wieder an die richtige Ausführungsposition zu bringen .

2.3. Warum sind der Stapel der virtuellen Maschine und der lokale Methodenstapel privat?

  • Stapel der virtuellen Maschine: Jede Java-Methode erstellt bei der Ausführung einen Stapelrahmen, um die lokale Variablentabelle, den Operandenstapel, die Konstantenpoolreferenz und andere Informationen zu speichern. Der Prozess vom Methodenaufruf bis zum Abschluss der Ausführung entspricht dem Prozess des Pushens und Einfügens eines Stapelrahmens in den Stapel der Java Virtual Machine.
  • Lokaler Methodenstapel: spielt eine sehr ähnliche Rolle wie der Stapel der virtuellen Maschine, der Unterschied ist: Der Stapel der virtuellen Maschine dient der virtuellen Maschine zum Ausführen von Java-Methoden (d. h. Bytecode) und der lokale Methodenstapel bedient die von der virtuellen Maschine verwendeten nativen Methoden. Kombiniert mit dem Java Virtual Machine Stack in der HotSpot Virtual Machine.

Um sicherzustellen, dass andere Threads nicht auf lokale Variablen im Thread zugreifen , sind der Stapel der virtuellen Maschine und der Stapel lokaler Methoden Thread-privat.

2.4. Ein einfaches Verständnis des Heap- und Methodenbereichs in einem Satz

Der Heap- und Methodenbereich sind Ressourcen, die von allen Threads gemeinsam genutzt werden. Dabei ist der Heap der größte Teil des Speichers, der hauptsächlich zum Speichern neu erstellter Objekte verwendet wird (hier wird allen Objekten Speicher zugewiesen). Der Methodenbereich wird hauptsächlich zum Speichern geladener Klasseninformationen, Konstanten, statischer Variablen und vom Just kompilierten Code verwendet -In-Time-Compiler und andere Daten.

3. Sprechen Sie über den Unterschied zwischen Parallelität und Parallelität?

  • Parallelität: Gleichzeitig Zeitraum, mehrere Alle Aufgaben werden ausgeführt (nicht unbedingt gleichzeitig pro Zeiteinheit);
  • Parallel: Mehrere Aufgaben werden gleichzeitig pro Zeiteinheit ausgeführt.

4. Warum Multithreading verwenden?

Lassen Sie uns allgemein darüber sprechen:

  • Von der Unterseite des Computers: Ein Thread kann mit einem einfachen Prozess verglichen werden, der die kleinste Einheit der Programmausführung darstellt. Die Kosten für das Umschalten und Planen zwischen Threads sind weitaus geringer als die eines Prozesses. Darüber hinaus bedeutet das Multi-Core-CPU-Zeitalter, dass mehrere Threads gleichzeitig ausgeführt werden können, was den Aufwand für den Thread-Kontextwechsel verringert.
  • Aus der Perspektive aktueller Internet-Entwicklungstrends: Heutige Systeme erfordern häufig Millionen oder sogar Dutzende Millionen Parallelität, und Multithread-Parallelitätsprogrammierung ist die Grundlage für die Entwicklung von Systemen mit hoher Parallelität. Die Verwendung von Multithreading-Mechanismen kann die allgemeine Parallelität und Leistung des Systems erheblich verbessern.

Lassen Sie uns tiefer in die unterste Schicht des Computers eintauchen:

  • Single-Core-Ära: In der Single-Core-Ära Multi-Threading Hauptsächlich ging es darum, die Gesamtauslastung der CPU und der E/A-Geräte zu verbessern. Beispiel: Wenn nur ein Thread vorhanden ist, ist das E/A-Gerät im Leerlauf, wenn die CPU Berechnungen durchführt. Wir können einfach sagen, dass die Auslastung beider derzeit bei etwa 50 % liegt. Bei zwei Threads ist es jedoch anders. Wenn ein Thread CPU-Berechnungen durchführt, kann der andere Thread E/A-Vorgänge ausführen, sodass die Auslastung der beiden im Idealfall 100 % erreichen kann.
  • Multi-Core-Ära: Multi-Threading im Multi-Core-Zeitalter dient hauptsächlich der Verbesserung der CPU-Auslastung. Beispiel: Wenn wir eine komplexe Aufgabe berechnen möchten und nur einen Thread verwenden, wird nur ein CPU-Kern der CPU genutzt. Durch das Erstellen mehrerer Threads können mehrere CPU-Kerne genutzt werden, was die CPU-Auslastung verbessert.

5. Welche Probleme können bei der Verwendung von Multithreading auftreten?

Der Zweck der gleichzeitigen Programmierung besteht darin, die Ausführungseffizienz zu verbessern des Programms Verbessern Sie die Programmlaufgeschwindigkeit, aber die gleichzeitige Programmierung verbessert nicht immer die Programmlaufgeschwindigkeit, und bei der gleichzeitigen Programmierung können viele Probleme auftreten, z. B. Speicherverluste, Kontextwechsel, Deadlocks und durch Hardware- und Softwarefragen begrenzte ungenutzte Ressourcen.

6. Sprechen Sie über den Lebenszyklus und den Status von Threads?

Ein Java-Thread kann nur zu einem bestimmten Zeitpunkt im Folgenden sein sein laufender Lebenszyklus Einer der 6 verschiedenen Zustände (Bildquelle „Die Kunst der gleichzeitigen Java-Programmierung“, Abschnitt 4.1.4).

Allgemeine Fragen zu Java-Parallelität in Vorstellungsgesprächen (Zusammenfassung)

Threads werden während des Lebenszyklus nicht in einem bestimmten Zustand fixiert, sondern wechseln zwischen verschiedenen Zuständen, während der Code ausgeführt wird. Die Zustandsänderungen von Java-Threads sind in der folgenden Abbildung dargestellt (Bildquelle „Die Kunst der gleichzeitigen Java-Programmierung“, Abschnitt 4.1.4):

Allgemeine Fragen zu Java-Parallelität in Vorstellungsgesprächen (Zusammenfassung)

Wie aus dem ersichtlich ist Abbildung oben: Nachdem der Thread erstellt wurde, befindet er sich im Status NEW (neu) und wird nach dem Aufruf der Methode start() ausgeführt. Der Thread befindet sich jetzt im Status READY (ausführbar) Staat. Ein Thread im ausführbaren Zustand befindet sich im Zustand RUNNING , nachdem er die CPU-Zeitscheibe (Zeitscheibe) erhalten hat.

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

Allgemeine Fragen zu Java-Parallelität in Vorstellungsgesprächen (Zusammenfassung)

当线程执行 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,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。

Allgemeine Fragen zu Java-Parallelität in Vorstellungsgesprächen (Zusammenfassung)

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

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教程

Das obige ist der detaillierte Inhalt vonAllgemeine Fragen zu Java-Parallelität in Vorstellungsgesprächen (Zusammenfassung). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen