Cet article résume les questions d'entretien de base courantes sur la concurrence Java pour tout le monde. Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer. J'espère qu'il sera utile à tout le monde.
1. Que sont les threads et les processus ?
1.1. Qu'est-ce qu'un processus ?
Un processus est un processus d'exécution d'un programme et constitue l'unité de base permettant au système d'exécuter des programmes, le processus est donc dynamique. L'exécution d'un programme sur le système est le processus depuis la création, l'exploitation jusqu'à la mort d'un processus.
En Java, lorsque nous démarrons la fonction principale, nous démarrons en fait un processus JVM, et le thread où se trouve la fonction principale est un thread de ce processus, également appelé thread principal.
Comme le montre la figure ci-dessous, en affichant le Gestionnaire des tâches dans Windows, nous pouvons clairement voir le processus en cours d'exécution dans la fenêtre (l'exécution du fichier .exe).
1.2. Qu'est-ce qu'un fil de discussion ?
Un fil de discussion est similaire à un processus, mais un le thread est un processus différent d'un processus Unités d'exécution plus petites. Un processus peut générer plusieurs threads lors de son exécution. À la différence d'un processus, plusieurs threads du même type partagent les ressources heap et zone de méthode du processus, mais chaque thread a son propre compteur de programme, La pile de machines virtuelles et la pile de méthodes locales , donc lorsque le système génère un thread ou bascule entre les threads, la charge est beaucoup plus petite que celle du processus. De ce fait, les threads le sont également. C’est ce qu’on appelle un processus léger.
Les programmes Java sont intrinsèquement des programmes multithreads. Nous pouvons utiliser JMX pour voir quels threads ont un programme Java ordinaire. Le code est le suivant.
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()); } } }
La sortie du programme ci-dessus est la suivante (le contenu de la sortie peut être différent, ne vous inquiétez pas trop du rôle de chaque thread ci-dessous, sachez simplement que le thread principal exécute la méthode principale) :
[5] Attach Listener //添加事件 [4] Signal Dispatcher // 分发处理给 JVM 信号的线程 [3] Finalizer //调用对象 finalize 方法的线程 [2] Reference Handler //清除 reference 线程 [1] main //main 线程,程序入口
À partir de la sortie ci-dessus, le contenu peut être vu : L'exécution d'un programme Java est le thread principal et plusieurs autres threads s'exécutant simultanément .
2. Veuillez décrire brièvement la relation, les différences, les avantages et les inconvénients entre les threads et les processus ?
La relation entre les processus et les threads du point de vue de la JVM
Illustration de la relation entre les processus et threads
L'image ci-dessous est la zone de mémoire Java. Grâce à l'image ci-dessous, nous pouvons parler de la relation entre les threads et les processus du point de vue de la JVM. Si vous ne savez pas grand chose sur la zone mémoire Java (zone de données d'exécution), vous pouvez lire cet article : "Probablement l'article le plus clair qui explique la zone mémoire Java"
Comme le montre l'image ci-dessus : il peut y avoir plusieurs threads dans un processus, et plusieurs threads partagent le tas et zone de méthode (métaespace après JDK1.8) ressources, mais chaque thread a son propre compteur de programme , pile de machines virtuelles et pile de méthodes locale.
Résumé : Les threads sont des processus divisés en unités d'exécution plus petites. La plus grande différence entre les threads et les processus est que chaque processus est fondamentalement indépendant, mais chaque thread ne l'est pas nécessairement, car les threads d'un même processus sont très susceptibles de s'influencer mutuellement. L'exécution des threads entraîne une petite surcharge, mais elle n'est pas propice à la gestion et à la protection des ressources ; l'inverse est vrai pour les processus
Ce qui suit est une extension de ce point de connaissance ! Réfléchissons à cette question : pourquoi lecompteur de programmes, la pile de machines virtuelles et la pile de méthodes locales sont-elles privées aux threads ? Pourquoi la zone de tas et la zone de méthode sont-elles partagées par les threads ?
2.2.Pourquoi le compteur de programme est-il privé ?
Le compteur de programme a principalement les deux fonctions suivantes :le restaurer à la position d'exécution correcte après le changement de thread .
2.3.Pourquoi la pile de machines virtuelles et la pile de méthodes locales sont-elles privées ?
Ainsi, afin de garantir que les variables locales du thread ne sont pas accessibles par d'autres threads , la pile de machine virtuelle et la pile de méthodes locales sont privées de thread.
2.4. Une compréhension simple de la zone de tas et de méthode en une phrase
La zone de tas et de méthode sont des ressources partagées par tous les threads, où le tas est le processus. La plus grande partie de la mémoire est principalement utilisée pour stocker les objets nouvellement créés (tous les objets se voient ici allouer de la mémoire). La zone de méthode est principalement utilisée pour stocker les informations de classe chargées, les constantes, les variables statiques et le code compilé par le juste. -compilateur à temps et autres données.
3. Parlez de la différence entre la concurrence et le parallélisme ?
4. Pourquoi utiliser le multi-threading ?
Parlons-en en général :
Approfondissons la couche inférieure de l'ordinateur :
5. Quels problèmes peuvent survenir lors de l'utilisation du multi-threading ?
Le but de la programmation simultanée est d'améliorer l'efficacité de l'exécution. du programme Améliore la vitesse d'exécution du programme, mais la programmation simultanée n'améliore pas toujours la vitesse d'exécution du programme, et la programmation simultanée peut rencontrer de nombreux problèmes, tels que : des fuites de mémoire, des changements de contexte, des blocages et des ressources inactives limitées par des questions matérielles et logicielles.
6. Parler du cycle de vie et de l'état des threads ?
Un thread Java ne peut être dans ce qui suit qu'à un moment précis dans son cycle de vie en cours d'exécution L'un des 6 états différents (source de l'image "The Art of Java Concurrent Programming" Section 4.1.4).
Les threads ne sont pas fixés dans un certain état pendant le cycle de vie mais basculent entre différents états au fur et à mesure que le code est exécuté. Les changements d'état des threads Java sont illustrés dans la figure ci-dessous (source de l'image "The Art of Java Concurrent Programming" Section 4.1.4) :
Comme le montre le figure ci-dessus : une fois le thread créé, il sera dans l'état NEW (nouveau) , il commence à s'exécuter après avoir appelé la méthode start()
. Le thread est maintenant dans l'état READY (exécutable) <.> état. Un thread à l'état exécutable est dans l'état RUNNING après avoir obtenu la tranche de temps CPU (timeslice).
操作系统隐藏 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(终止) 状态。
7. 什么是上下文切换?
多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。
概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。任务从保存到再加载的过程就是一次上下文切换。
上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。
Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。
8. 什么是线程死锁?如何避免死锁?
8.1. 认识线程死锁
多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
下面通过一个例子来说明线程死锁,代码模拟了上图的死锁的情况 (代码来源于《并发编程之美》):
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 休眠结束了都开始企图请求获取对方的资源,然后这两个线程就会陷入互相等待的状态,这也就产生了死锁。上面的例子符合产生死锁的四个必要条件。
学过操作系统的朋友都知道产生死锁必须具备以下四个条件:
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() 方法区别和共同点?
10. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?
这是另一个非常经典的 java 多线程面试问题,而且在面试中会经常被问到。很简单,但是很多人都会答不上来!
new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。
推荐教程:java教程
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!