Multithreading
1. Notions connexes
Concurrence et parallélisme
Parallèle (parallèle) : fait référence à plusieurs tâches événementielles se produisant en même temps (simultanément).
Concurrence : fait référence à deux ou plusieurs événements se produisant dans la même petite période de temps .L'exécution simultanée de programmes peut utiliser pleinement les ressources du processeur dans des conditions limitées.
CPU monocœur : uniquement simultané
CPU multicœur : parallèle + simultané
Threads et processus
-
Programme : Afin d'accomplir une certaine tâche et fonction, choisissez un ensemble d'instructions écrites dans un langage de programmation.
-
Logiciel : 1 ou plusieurs applications + matériels et fichiers ressources associés constituent un système logiciel.
-
Un processus est une description du processus en cours d'exécution (création-exécution-mort) d'un programme. Le système crée un processus pour chaque programme en cours d'exécution et alloue des ressources système indépendantes, telles que de l'espace mémoire, au processus.
-
Thread : Un thread est une unité d'exécution dans un processus, qui est responsable de l'exécution de la tâche d'exécution du programme en cours. Il y a au moins un thread dans un processus. Il peut y avoir plusieurs threads dans un processus, et ce programme d'application peut également être appelé un programme multi-thread à ce stade. Le multithreading permet aux programmes de s'exécuter simultanément et d'utiliser pleinement les ressources du processeur.
Question de l'entretien : Un processus est la plus petite unité de planification du système d'exploitation et d'allocation des ressources, et un thread est la plus petite unité de planification du processeur. Différents processus ne partagent pas la mémoire. Le coût des échanges de données et de la communication entre les processus est élevé. Différents threads partagent la mémoire du même processus. Bien sûr, différents threads ont également leur propre espace mémoire indépendant. Pour la zone de méthode, la mémoire d'un même objet dans le tas peut être partagée entre les threads, mais les variables locales de la pile sont toujours indépendantes.
Avantages et scénarios d'application du multithreading
- L'avantage principal:
- Tirez pleinement parti des tranches de temps d'inactivité du processeur pour répondre aux demandes des utilisateurs dans les plus brefs délais. C'est pour que le programme réponde plus rapidement.
- Scénario d'application :
- multitâche. Lorsque plusieurs utilisateurs demandent le serveur, le programme serveur peut ouvrir plusieurs threads pour traiter la demande de chaque utilisateur séparément sans affecter les uns les autres.
- Traitement d'une seule grande tâche. Pour télécharger un fichier volumineux, vous pouvez ouvrir plusieurs threads à télécharger ensemble, ce qui réduit le temps de téléchargement global.
planification des threads
Fait référence à la manière dont les ressources du processeur sont allouées aux différents threads. Deux méthodes courantes de planification des threads :
-
planification en temps partagé
Tous les threads utilisent le CPU à tour de rôle, et chaque thread utilise le temps CPU de manière uniforme.
-
planification préemptive
Donner la priorité aux threads avec une priorité élevée pour utiliser le CPU. Si les threads ont la même priorité, un sera sélectionné au hasard (thread randomness). Java utilise une méthode de planification préemptive .
2. Création et démarrage du thread
Hériter de la classe Thread
Étapes pour créer et démarrer le multithreading en héritant de la classe Thread :
- Définissez la sous-classe de la classe Thread et réécrivez la méthode run() de cette classe. Le corps de la méthode run() représente la tâche que le thread doit accomplir, de sorte que la méthode run() est appelée le corps d'exécution du thread .
- Créer une instance de la sous-classe Thread, c'est-à-dire créer un objet thread
- Appelez la méthode start() de l'objet thread pour démarrer le thread
Notes sur l'analyse d'exécution multi-thread :
-
L'appel manuel de la méthode run n'est pas le moyen de démarrer un thread, c'est juste un appel de méthode normal.
-
Une fois que la méthode start a démarré le thread, la méthode run est appelée et exécutée par la JVM.
-
Ne démarrez pas le même thread à plusieurs reprises, sinon une exception sera levée
IllegalThreadStateException
-
N'utilisez pas l'unité Junit pour tester le multi-threading, il n'est pas pris en charge, après la fin du thread principal, il appellera pour
System.exit()
quitter la JVM directement ;
Implémenter l'interface Runnable
- Définissez la classe d'implémentation de l'interface Runnable et réécrivez la méthode run() de l'interface. Le corps de la méthode run() est également le corps d'exécution du thread.
- Créez une instance de la classe d'implémentation Runnable et utilisez cette instance comme cible de Thread pour créer un objet Thread, qui est le véritable objet thread.
- Appelez la méthode start() de l'objet thread pour démarrer le thread.
Comparaison de deux manières de créer des threads
-
La classe Thread elle-même implémente également l'interface Runnable. La méthode run provient de l'interface Runnable et la méthode run est également la tâche de thread réelle à exécuter.
public class Thread implements Runnable { }
-
Étant donné que les classes Java sont à héritage unique, la manière d'hériter de Thread a la limitation de l'héritage unique, mais elle est plus simple à utiliser.
-
La façon d'implémenter l'interface Runnable évite la limitation de l'héritage unique et peut créer plusieurs objets de threadPartager un objet de classe d'implémentation Runnable (classe de tâche de thread), afin de faciliter l'exécution de tâches multi-threadpartager des données.
Fil de création d'objet de classe interne anonyme
La création de threads au moyen d'objets anonymes de classe interne n'est pas une nouvelle façon de créer des threads, mais lorsque la tâche de thread n'a besoin d'être exécutée qu'une seule fois, nous n'avons pas besoin de créer des classes de threads séparément, nous pouvons utiliser des objets anonymes :
new Thread("新的线程!"){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(getName()+":正在执行!"+i);
}
}
}.start();
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+":" + i);
}
}
}).start();
3. Classe de thread
Méthode constructive
- public Thread() : alloue un nouvel objet thread.
- public Thread(String name) : alloue un nouvel objet thread avec le nom spécifié.
- public Thread(Runnable target) : alloue un nouvel objet thread avec la cible spécifiée.
- public Thread(Runnable target, String name) : alloue un nouvel objet thread avec la cible spécifiée et spécifie le nom.
Les threads utilisent la méthode de base
-
public void run() : La tâche à effectuer par ce thread est définie ici.
-
public String getName() : Récupère le nom du thread actuel.
-
public static Thread currentThread() : renvoie une référence à l'objet thread en cours d'exécution.
-
public final boolean isAlive() : teste si le thread est actif. Actif si le thread a été démarré et n'est pas encore terminé.
-
public final int getPriority() : renvoie la priorité du thread
-
public final void setPriority(int newPriority): change la priorité du thread
- Chaque thread a une certaine priorité, et les threads avec une priorité plus élevée obtiendront plus d'opportunités d'exécution. Par défaut, chaque thread a la même priorité que le thread parent qui l'a créé. La classe Thread fournit les classes de méthode setPriority(int newPriority) et getPriority() pour définir et obtenir la priorité du thread, où la méthode setPriority nécessite un entier et la plage est comprise entre [1,10]. Il est généralement recommandé de définissez les trois priorités des constantes de la classe Thread :
- MAX_PRIORITY (10) : priorité la plus élevée
- MIN_PRIORITY (1) : priorité la plus faible
- NORM_PRIORITY (5) : priorité normale, par défaut le thread principal a une priorité normale.
Méthodes courantes de contrôle des threads
-
public void start() : provoque le démarrage de l'exécution de ce thread ; la machine virtuelle Java appelle la méthode run de ce thread.
-
public static void sleep(long millis): Thread sleep, qui met le thread en cours d'exécution en pause (arrête temporairement son exécution) pendant le nombre de millisecondes spécifié.
-
public static void yield() : politesse du thread, yield fait simplement perdre temporairement au thread actuel le droit de s'exécuter, laisse le planificateur de threads du système reprogrammer, en espérant que d'autres threads avec la même priorité ou une priorité supérieure au thread actuel puissent avoir la chance de s'exécuter , mais ceci Il n'y a aucune garantie. Il est tout à fait possible que lorsqu'un thread appelle la méthode yield pour s'arrêter, le planificateur de thread le programme pour qu'il se réexécute.
-
void join() : Rejoindre un thread, ajouter un nouveau thread au thread en cours, attendre que le thread joint se termine avant de continuer à exécuter le thread en cours.
void join(long millis) : attend que ce thread se termine jusqu'à millis millis millisecondes. Si le millis de temps est écoulé, il n'y aura plus d'attente.
void join(long millis, int nanos) : Attendez que le thread se termine jusqu'à millis millis + nanos nanosecondes.
-
public final void stop() : force le thread à arrêter de s'exécuter. Cette méthode est dangereuse, obsolète et ne doit pas être utilisée.
- L'appel de la méthode stop() arrêtera immédiatement tout le travail restant dans la méthode run(), y compris ceux de l'instruction catch ou finally, et lancera une exception ThreadDeath (généralement cette exception ne nécessite pas de capture explicite). Les travaux de nettoyage ne peuvent pas être terminés, tels que la fermeture de fichiers, de bases de données, etc.
- L'appel de la méthode stop() libère immédiatement tous les verrous détenus par le thread, ce qui entraîne des données non synchronisées et une incohérence des données.
-
public void interrupt() : L'interruption du thread marque en fait le thread comme une interruption et n'arrête pas réellement l'exécution du thread.
-
public statique booléen interrompu () : vérifiez l'état interrompu du thread, l'appel de cette méthode effacera l'état interrompu (drapeau).
-
public boolean isInterrupted() : vérifie l'état interrompu du fil, n'effacera pas l'état interrompu (drapeau)
-
public void setDaemon(boolean on) : définit le thread en tant que thread démon. Il doit être défini avant le démarrage du thread, sinon
IllegalThreadStateException
une exception sera signalée.- Le thread démon sert principalement d'autres threads. Lorsqu'il n'y a pas de thread non démon en cours d'exécution dans le programme, le thread démon termine également l'exécution. Le ramasse-miettes JVM est également un thread démon.
-
public boolean isDaemon() : vérifie si le thread actuel est un thread démon.
-
Le rôle de volatile est de s'assurer que certaines instructions ne seront pas omises en raison de l'optimisation du compilateur. La variable volatile signifie que la variable peut être modifiée de manière inattendue. Relisez attentivement la valeur de cette variable à chaque fois au lieu d'utiliser save Une sauvegarde dans le registre, afin que le compilateur n'assume pas la valeur de cette variable.
cycle de vie des fils
Cinq états de thread du modèle de threading traditionnel
Les six états de thread définis par JDK définissent une classe d'énumération à l'intérieur de la classe pour décrire les six états du thread
:java.lang.Thread
public enum State {
NEW,
RUNNABLE,
BLOCKED,
WAITING,
TIMED_WAITING,
TERMINATED;
}
Description : lors du retour de WAITING
ou vers l'état, s'il s'avère que le thread actuel n'a pas obtenu le verrouillage du moniteur, il passera immédiatement à l'état.TIMED_WAITING
Runnable
BLOCKED
sécurité des fils
Lorsque nous utilisons plusieurs threads pour accéder à la même ressource (il peut s'agir de la même variable, du même fichier, du même enregistrement, etc.), mais s'il y a des opérations de lecture et d'écriture sur la ressource dans plusieurs threads, il y aura incohérence des données problèmes avant et après, ce qui est un problème de sécurité des threads.
Les problèmes de sécurité des threads entraînent
- Les variables locales ne peuvent pas être partagées : les variables locales sont indépendantes à chaque fois que la méthode est appelée, donc les données de run() de chaque thread sont indépendantes, pas des données partagées.
- Les variables d'instance des différents objets ne sont pas partagées : les variables d'instance des différents objets d'instance sont indépendantes.
- les variables statiques sont partagées
- Les variables d'instance d'un même objet sont partagées
Résumé : des problèmes de sécurité des threads surviennent en raison des conditions suivantes
- exécution multithread
- partager des données
- Plusieurs déclarations fonctionnent sur des données partagées
Solution au problème de sécurité des threads
Méthode de synchronisation : Le mot clé synchronized modifie directement la méthode, indiquant qu'un seul thread peut entrer dans cette méthode à la fois, et que d'autres threads attendent à l'extérieur.
public synchronized void method(){
可能会产生线程安全问题的代码
}
Bloc de code synchronisé : Le mot clé synchronized peut être utilisé devant un certain bloc, indiquant que seules les ressources de ce bloc s'excluent mutuellement.
synchronized(同步锁){
需要同步操作的代码
}
Verrouiller la sélection d'objets
préféré cecisuivie parclasse.classepeut également être" "
objet chaîne vide
Objet de verrouillage de synchronisation :
- Les objets de verrouillage peuvent être de n'importe quel type.
- Plusieurs objets thread utilisent le même verrou.
L'objet verrou du bloc de code synchronisé
- Dans le bloc de code statique : utilisez l'objet Class de la classe courante
- Dans les blocs de code non statiques : il est d'usage de considérer cela en premier, mais faites attention à ce que le même
La portée du verrou est trop petite : il ne peut pas résoudre les problèmes de sécurité et toutes les instructions qui fonctionnent sur des ressources partagées doivent être synchronisées.
La portée du verrou est trop grande : parce qu'une fois qu'un thread saisit le verrou, les autres threads ne peuvent qu'attendre, la portée est donc trop grande, l'efficacité sera réduite et les ressources du processeur ne peuvent pas être utilisées raisonnablement.
Problèmes de sécurité des threads du modèle de conception singleton
1. Le style chinois affamé n'a pas de problèmes de sécurité des fils
Style chinois affamé : créez des objets dès que vous vous levez
2. Problèmes de sécurité des threads de style paresseux
public class Singleton {
private static Singleton ourInstance;
public static Singleton getInstance() {
//一旦创建了对象,之后再次获取对象,都不会再进入同步代码块,提升效率
if (ourInstance == null) {
//同步锁,锁住判断语句与创建对象并赋值的语句
synchronized (Singleton.class) {
if (ourInstance == null) {
ourInstance = new Singleton();
}
}
}
return ourInstance;
}
private Singleton() {
}
}
En attente du mécanisme de réveil
Lorsqu'un thread remplit une certaine condition, il entre dans l'état d'attente ( wait() / wait(time) ), attend que d'autres threads exécutent leur code spécifié, puis le réveille ( notify() ); ou vous pouvez spécifier l'attente time , Attendez que l'heure se réveille automatiquement ; lorsqu'il y a plusieurs threads en attente, si nécessaire, vous pouvez utiliser notifyAll() pour réveiller tous les threads en attente. wait/notify est un mécanisme de coordination entre les threads.
- attendre : le thread n'est plus actif, ne participe plus à la planification et entre dans le jeu d'attente, de sorte qu'il ne gaspillera pas les ressources du processeur et ne concourra pas pour les verrous. L'état du thread à ce moment est WAITING ou TIMED_WAITING. Il attend également que d'autres threads effectuent une action spéciale , c'est-à-dire " notifier " ou lorsque le temps d'attente est écoulé, le thread en attente sur cet objet est libéré du jeu d'attente et réintègre la file d'attente de répartition (file d'attente prête) ) milieu
- notifier : sélectionnez un thread dans le jeu d'attente de l'objet notifié à libérer ;
- notifyAll : libère tous les threads sur le jeu d'attente de l'objet notifié.
Remarque :
Une fois le thread notifié réveillé, il se peut qu'il ne puisse pas reprendre son exécution immédiatement, car l'endroit où il a été interrompu se trouvait dans le bloc de synchronisation et, à ce moment, il ne détient plus le verrou. Elle doit donc essayer de acquérir à nouveau le verrou (probablement face à une autre concurrence Thread), ce n'est qu'après le succès que l'exécution peut reprendre à l'endroit après que la méthode d'attente a été initialement appelée.
Résumé comme suit :
- Si le verrou peut être acquis, le thread passe de l'état WAITING à l'état RUNNABLE (exécutable) ;
- Sinon, le thread passe de l'état WAITING à l'état BLOCKED (en attente de verrouillage)
Les détails auxquels il faut prêter attention lors de l'appel des méthodes d'attente et de notification
- La méthode d'attente et la méthode de notification doivent être appelées par le même objet verrou. Parce que : l'objet de verrouillage correspondant peut réveiller le thread après l'appel de la méthode d'attente avec le même objet de verrouillage via notifier.
- La méthode wait et la méthode notify appartiennent aux méthodes de la classe Object. Parce que : l'objet verrou peut être n'importe quel objet, et la classe de n'importe quel objet hérite de la classe Object.
- La méthode d'attente et la méthode de notification doivent être utilisées dans un bloc de code de synchronisation ou une fonction de synchronisation, et ces deux méthodes doivent être appelées via l'objet de verrouillage.
Libérer le fonctionnement du verrouillage et l'impasse
1. L'opération de libération du verrou
-
L'exécution de la méthode de synchronisation et du bloc de code de synchronisation du thread actuel se termine.
-
Une erreur ou une exception non gérée se produit dans le bloc de code de synchronisation ou la méthode de synchronisation du thread actuel, ce qui entraîne la fin anormale du thread actuel.
-
Le thread actuel exécute la méthode wait() de l'objet de verrouillage dans le bloc de code de synchronisation et la méthode de synchronisation, le thread actuel est suspendu et le verrou est libéré.
2. Impasse
Différents threads verrouillent l'objet moniteur de synchronisation requis par l'autre partie et ne le libèrent pas. Lorsqu'ils attendent que l'autre partie abandonne en premier, un blocage de thread se forme. Une fois qu'un blocage se produit, le programme entier ne sera ni anormal ni ne donnera aucune invite, mais tous les threads sont bloqués et ne peuvent pas continuer.
3. Question d'entretien : la différence entre les méthodes sleep() et wait()
(1) sleep() ne libère pas le verrou, wait() libère le verrou
(2) sleep () spécifie le temps de sommeil, wait () peut spécifier le temps ou attendre indéfiniment jusqu'à notifier ou notifier tout
(3) sleep() est une méthode statique déclarée dans la classe Thread, et la méthode wait est déclarée dans la classe Object
Parce que nous appelons la méthode wait () est appelée par l'objet de verrouillage, et le type de l'objet de verrouillage est n'importe quel type d'objet. Ensuite, les méthodes que vous souhaitez attribuer à n'importe quel type d'objet ne peuvent être déclarées que dans la classe Object.
pratique
Deux threads sont nécessaires pour imprimer des lettres en même temps, et chaque thread peut imprimer 3 lettres en continu. Deux fils impriment alternativement, un fil imprime la forme minuscule de la lettre et un fil imprime la forme majuscule de la lettre, mais les lettres sont consécutives. Une fois que la lettre est passée à z, revenez à a.
public class PrintLetterDemo {
public static void main(String[] args) {
// 2、创建资源对象
PrintLetter p = new PrintLetter();
// 3、创建两个线程打印
new Thread("小写字母") {
public void run() {
while (true) {
p.printLower();
try {
Thread.sleep(1000);// 控制节奏
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
new Thread("大写字母") {
public void run() {
while (true) {
p.printUpper();
try {
Thread.sleep(1000);// 控制节奏
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
// 1、定义资源类
class PrintLetter {
private char letter = 'a';
public synchronized void printLower() {
for (int i = 1; i <= 3; i++) {
System.out.println(Thread.currentThread().getName() + "->" + letter);
letter++;
if (letter > 'z') {
letter = 'a';
}
}
this.notify();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized void printUpper() {
for (int i = 1; i <= 3; i++) {
System.out.println(Thread.currentThread().getName() + "->" + (char) (letter - 32));
letter++;
if (letter > 'z') {
letter = 'a';
}
}
this.notify();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}