1. Introduction multi-lancement
Dans la programmation, nous ne pouvons pas éviter les problèmes de programmation multiples, car un traitement simultané est requis dans la plupart des systèmes commerciaux. S'il se trouve dans des scénarios simultanés, le multi-threading est très important. De plus, lors de notre entretien, l'intervieweur nous pose généralement des questions sur le multi-lancement, telles que: comment créer un fil? Nous répondons généralement de cette façon, il existe deux méthodes principales. Le premier est: hériter de la classe de thread et réécrire la méthode d'exécution; Le second est: implémenter l'interface Runnable et réécrire la méthode d'exécution. Ensuite, l'intervieweur demandera certainement quels sont les avantages et les inconvénients de ces deux méthodes. Quoi qu'il en soit, nous arriverons à une conclusion, c'est-à-dire la deuxième façon d'utiliser, car les avocats orientés objet préconisent moins d'héritage et essaient d'utiliser autant que possible les combinaisons.
Pour le moment, nous pouvons également penser à quoi faire si nous voulons obtenir la valeur de retour des multiples? Sur la base des connaissances que nous avons appris plus, nous penserons à implémenter l'interface appelée et à réécrire la méthode d'appel. Comment tant de threads sont-ils utilisés dans des projets réels? De combien de façons ont-ils?
Tout d'abord, jetons un coup d'œil à un exemple:
Il s'agit d'une méthode simple pour créer des multiples multiples, ce qui est facile à comprendre. Dans l'exemple, selon différents scénarios commerciaux, nous pouvons transmettre différents paramètres dans Thread () pour implémenter différentes logiques commerciales. Cependant, le problème exposé par cette méthode de création de multiples multiples est de créer des threads à plusieurs reprises, et il doit être détruit après la création de threads. Si les exigences pour les scénarios simultanées sont faibles, cette méthode semble être OK, mais dans les scénarios de concurrence élevés, cette méthode n'est pas possible, car la création de threads est très consommatrice de ressources. Donc, selon l'expérience, la bonne façon est d'utiliser la technologie de pool de threads. JDK fournit une variété de types de piscines de fil pour que nous puissions choisir. Pour des méthodes spécifiques, vous pouvez consulter la documentation JDK.
Ce que nous devons noter dans ce code, c'est que les paramètres passés représentent le nombre de threads que nous avons configurés. Est-ce que plus est le mieux? Sûrement pas. Parce que lors de la configuration du nombre de threads, nous devons pleinement considérer les performances du serveur. S'il y a plus de configurations de thread, les performances du serveur peuvent ne pas être excellentes. Habituellement, les calculs effectués par la machine sont déterminés par le nombre de threads. Lorsque le nombre de threads atteint le pic, le calcul ne peut pas être effectué. Si c'est la logique métier qui consomme CPU (plus de calculs), le nombre de threads et de cœurs atteindra son apogée. Si c'est la logique métier qui consomme des E / S (bases de données d'exploitation, téléchargement de fichiers, téléchargement, etc.), plus il y a de threads, plus il y a de threads, cela aidera à améliorer les performances dans un certain sens.
Une autre formule pour définir le nombre de threads:
Y = n * ((a + b) / a), où n: le nombre de cœurs de CPU, a: le temps de calcul du programme lorsque le thread est exécuté, b: le temps de blocage du programme lorsque le thread est exécuté. Avec cette formule, la configuration du nombre de threads du pool de threads sera contrainte et nous pouvons la configurer de manière flexible en fonction de la situation réelle de la machine.
2. Optimisation multithread et comparaison des performances
La technologie de filetage a été utilisée dans des projets récents, et j'ai rencontré beaucoup de problèmes lors de l'utilisation. En profitant de la popularité, je trierai les comparaisons de performances de plusieurs cadres multithreads. Ceux que nous avons maîtrisés sont à peu près divisés en trois types: le premier type: Threadpool (Thread Pool) + CountdownLatch (compteur de programme), le deuxième type: Frame Fork / Join et le troisième type de flux parallèle JDK8. Voici un résumé comparatif des performances multiples de ces méthodes.
Tout d'abord, supposons un scénario commercial où plusieurs objets de fichier sont générés en mémoire. Ici, le sommeil de 30 000 threads est provisoirement déterminé à simuler la logique commerciale de traitement des entreprises pour comparer les performances multi-lancers de ces méthodes.
1) simple
Cette méthode est très simple, mais le programme prend beaucoup de temps pendant le traitement et sera utilisé pendant longtemps, car chaque thread attend que le thread actuel soit exécuté avant qu'il ne soit exécuté. Cela n'a pas grand-chose à voir avec les multiples, donc l'efficacité est très faible.
Créez d'abord l'objet de fichier, le code est le suivant:
classe publique FileInfo {Private String FileName; // Nom de fichier Private String FileType; // Type de fichier String Private String FileSize; // Taille de fichier String Private FileMd5; // Code MD5 Private String FileVersionNo; // Numéro de version de fichier public FileInfo () {super (); } public FileInfo (String FileName, String FileType, String FileSize, String fileMd5, String fileVeversionNo) {super (); this.filename = nom de fichier; this.FileType = FileType; this.filesize = fileSize; this.filemd5 = fileMd5; this.fileversionNo = fileversionNo; } public String getFileName () {return filename; } public void setFileName (String filename) {this.filename = filename; } public String getFileType () {return fileType; } public void setFileType (String FileType) {this.fileType = fileType; } public String getFileSize () {return fileSize; } public void setFileSize (String FileSize) {this.filesize = fileSize; } public String getFileMD5 () {return fileMd5; } public void setFileMD5 (String fileMd5) {this.filemd5 = fileMd5; } public String getFileVersionNo () {return fileversionNo; } public void setFileVersionNo (String fileversionNo) {this.fileversionNo = fileversionNo; }Ensuite, simulez le traitement commercial, créez 30 000 objets de fichier, le thread dort pendant 1 ms et définit 1000 ms auparavant, et constate que le temps est très long et que l'éclipse entière est bloquée, alors changez l'heure à 1 ms.
Classe publique Test {Liste statique privée <FichierInfo> FileList = new ArrayList <FichierInfo> (); public static void main (String [] args) lève InterruptedException {createFileInfo (); Long startTime = System.CurrentTimemillis (); for (fileInfo fi: FileList) {Thread.Sleep (1); } Long EndTime = System.CurrentTimeMillis (); System.out.println ("Time de fil unique qui prend du temps:" + (fin de starttime) + "MS"); } private static void CreateFileInfo () {for (int i = 0; i <30000; i ++) {FileList.add (new FileInfo ("Front Photo of Id Card", "JPG", "101522", "MD5" + i, "1")); }}}Les résultats des tests sont les suivants:
On peut voir que la génération de 30 000 objets de fichiers prend beaucoup de temps, près d'une minute et que l'efficacité est relativement faible.
2) Threadpool (Thread Pool) + compte à rebours (compteur de programme)
Comme son nom l'indique, CountdownLatch est un compteur de thread. Son processus d'exécution est le suivant: Tout d'abord, la méthode Await () est appelée dans le thread principal, et le thread principal est bloqué, puis le compteur de programme est passé à l'objet de thread en tant que paramètre. Enfin, une fois que chaque thread a terminé l'exécution de la tâche, la méthode à rebours () est appelée pour indiquer l'achèvement de la tâche. Une fois le Countdown () exécuté plusieurs fois, le fil principal Await () sera invalide. Le processus de mise en œuvre est le suivant:
classe publique Test2 {private static ExecutorService exécutor = exécutor.newFixEdThreadPool (100); Compte à rebours statique privé CountdownLatch = nouveau compte à rebours (100); Liste statique privée <FichierInfo> FileList = new ArrayList <FichierInfo> (); Liste statique privée <list <FichierInfo>> list = new ArrayList <> (); public static void main (String [] args) lève InterruptedException {createFileInfo (); addList (); Long startTime = System.CurrentTimemillis (); int i = 0; for (list <leinfo> fi: list) {exécutor.submit (new FileUnnable (CountdownLatch, fi, i)); i ++; } CountdownLatch.Await (); Long EndTime = System.CurrentTimemillis (); exécuteur.shutdown (); System.out.println (i + "Threads prennent du temps:" + (début du temps) + "MS"); } private static void createFileInfo () {for (int i = 0; i <30000; i ++) {FileList.add (new FileInfo ("Front ID Card Photo", "JPG", "101522", "MD5" + i, "1")); }} private static void addList () {for (int i = 0; i <100; i ++) {list.add (fileList); }}}Classe FileUnnable:
/ ** * Multithreaded Processing * @author wangsj * * @param <T> * / public class filerunnable <T> implémente Runnable {private CountdownLatch CountDownLatch; Liste privée <T> Liste; Int privé I; public filerUnnable (CountdownLatch CountdownLatch, liste <T> List, int i) {super (); this.CountDownLatch = CountDownLatch; this.list = list; this.i = i; } @Override public void run () {for (t t: list) {try {thread.sleep (1); } catch (InterruptedException e) {e.printStackTrace (); } CountdownLatch.CountDown (); }}}Les résultats des tests sont les suivants:
3) Framework Fork / Join
JDK a commencé avec la version 7 et le cadre Fork / Join est apparu. D'un point de vue littéral, Fork se divise et se joint à la fusion, donc l'idée de ce cadre est. Divisez la tâche via Fork, puis rejoignez pour fusionner les résultats après l'exécution des caractères divisés et résumé. Par exemple, nous voulons calculer plusieurs nombres qui sont ajoutés en continu, 2 + 4 + 5 + 7 =? , Comment utilisons-nous le framework Fork / Join pour le terminer? L'idée est de diviser les tâches moléculaires. Nous pouvons diviser cette opération en deux sous-tâches, l'une calcule 2 + 4 et l'autre calcule 5 + 7. C'est le processus de Fork. Une fois le calcul terminé, les résultats du calcul de ces deux sous-tâches sont résumés et la somme est obtenue. Ceci est le processus de jointure.
Idée d'exécution du framework Fork / JOIN: Tout d'abord, divisez les tâches et utilisez la classe Fork pour diviser les tâches importantes en plusieurs sous-tâches. Ce processus de segmentation doit être déterminé en fonction de la situation réelle jusqu'à ce que les tâches divisées soient suffisamment faibles. Ensuite, la classe de jointure exécute la tâche et les sous-tâches divisées sont dans différentes files d'attente. Plusieurs threads obtiennent des tâches de la file d'attente et les exécutent. Les résultats d'exécution sont placés dans une file d'attente séparée. Enfin, le thread est démarré, les résultats sont obtenus dans la file d'attente et les résultats sont fusionnés.
Plusieurs classes sont utilisées pour utiliser le framework Fork / Join. Pour l'utilisation de la classe, vous pouvez vous référer à l'API JDK. En utilisant ce cadre, vous devez hériter de la classe ForkJointask. Habituellement, il vous suffit de hériter de sa sous-classe RecursiveTask ou RecursiveAction. RecursiveTask est utilisé pour des scènes avec des résultats de retour, et RecursiveAction est utilisé pour des scènes sans résultats de retour. L'exécution de FORKJOINTASK nécessite l'exécution de ForkJoinpool, qui est utilisée pour maintenir les sous-tâches divisées ajoutées à différentes files d'attente de tâches.
Voici le code d'implémentation:
Classe publique Test3 {Liste statique privée <FichierInfo> FileList = new ArrayList <FichierInfo> (); // Private Static Forkjoinpool Forkjoinpool = new Forkjoinpool (100); // Private Static Job <FileInfo> Job = nouveau Job <> (FileList.Size () / 100, FileLelist); public static void main (String [] args) {createFileInfo (); Long startTime = System.CurrentTimemillis (); Forkjoinpool forkjoinpool = new forkjoinpool (100); // diviser le travail de tâche <FichierInfo> Job = new Job <> (fileList.size () / 100, fileList); // Soumettez la tâche et renvoyez le résultat forkjointask <Integer> fjTRESULT = FORKJOINPOOL.SUMMIT (JOB); // Bloquer While (! Job.isDone ()) {System.out.println ("Tâche terminée!"); } Long EndTime = System.CurrentTimeMillis (); System.out.println ("Frame / join / joint framework chronométrant:" + (fin de starttime) + "MS"); } private static void createFileInfo () {for (int i = 0; i <30000; i ++) {FileList.add (new FileInfo ("Front ID Card Photo", "JPG", "101522", "MD5" + i, "1")); }}} / ** * Exécuter la classe de tâches * @author wangsj * * / classe publique Job <T> étend RecursiveTask <Integer> {private static final long SerialVersionUID = 1l; Count int privé; Liste privée <T> JOBLIST; Public Job (int count, liste <T> JOBLIST) {super (); this.count = count; this.joblist = joblist; } / ** * Exécutez la tâche, similaire à la méthode d'exécution qui implémente l'interface Runnable * / @Override Protected Integer calcul () {// diviser la tâche if (Joblist.size () <= count) {executeJob (); return Joblist.size (); } else {// Continuez à créer la tâche jusqu'à ce qu'elle puisse être décomposée et exécutée Liste <RecursiveTask <long>> fork = new LinkedList <CorsiveTask <long>> (); // Divit la tâche nucléique, ici la méthode de dichotomie est utilisée int countjob = jeblist.size () / 2; List <T> LeftList = joblist.sublist (0, countJob); List <t> droite = jeblist.sublist (countjob, jeblist.size ()); // Attribuez des tâches Job Leftjob = nouveau travail <> (count, LeftList); Job RightJob = new Job <> (Count, RightList); // Exécuter la tâche LeftJob.Fork (); RightJob.Fork (); return Integer.ParseInt (LeftJob.Join (). ToString ()) + Integer.ParseInt (RightJob.Join (). ToString ()); }} / ** * Exécuter la méthode de la tâche * / private void executeJob () {for (t job: joblist) {try {Thread.Sleep (1); } catch (InterruptedException e) {e.printStackTrace (); }}}Les résultats des tests sont les suivants:
4) JDK8 Streaming parallèle
Le flux parallèle est l'une des nouvelles caractéristiques de JDK8. L'idée est de transformer un flux exécuté séquentiellement en un flux simultané, qui est implémenté en appelant la méthode parallèle (). Le flux parallèle divise un flux en plusieurs blocs de données, utilise différents threads pour traiter les flux de différents blocs de données et fusionne enfin les résultats de traitement de chaque bloc de flux de données, semblable au cadre de fourche / jointure.
Le flux parallèle utilise le filetage public Forkjoinpool par défaut. Le nombre de threads est la valeur par défaut utilisée. Selon le nombre de noyaux de la machine, nous pouvons ajuster la taille des fils de manière appropriée. L'ajustement du nombre de threads est obtenu de la manière suivante.
System.SetProperty ("java.util.concurrent.forkjoinpool.common.Parallelism", "100");Ce qui suit est le processus d'implémentation du code, ce qui est très simple:
classe publique Test4 {Liste statique privée <FichierInfo> FileList = new ArrayList <FichierInfo> (); public static void main (String [] args) {// System.SetProperty ("java.util.concurrent.forkjoinpool.common.parallelism", "100"); createFileInfo (); Long startTime = System.CurrentTimemillis (); FileList.ParallelStream (). ForEach (e -> {try {Thread.Sleep (1);} catch (InterruptedException f) {f.printStackTrace ();}}); Long EndTime = System.CurrentTimemillis (); System.out.println ("JDK8 Temps de streaming parallèle:" + (fin de starttime) + "MS");} private static void createFileInfo () {for (int i = 0; i <30000; i ++) {FileList.Add (new FileInfo ("Front Photo of Id de carte "," jpg "," 101522 "," md5 "+ i," 1 ")); }}}Ce qui suit est le test. Le nombre de pools de threads n'est pas défini pour la première fois. La valeur par défaut est utilisée. Les résultats des tests sont les suivants:
Nous avons vu que le résultat n'est pas très idéal et prend beaucoup de temps. Ensuite, définissez le nombre de pools de threads, c'est-à-dire ajouter le code suivant:
System.SetProperty ("java.util.concurrent.forkjoinpool.common.Parallelism", "100");Ensuite, le test a été effectué et les résultats étaient les suivants:
Cette fois, cela prend moins de temps et est idéal.
3. Résumé
Pour résumer les situations ci-dessus, en utilisant un seul thread comme référence, le plus longtemps prend du temps le cadre de fourche / jointure natif. Bien que le nombre de pools de thread soit configuré ici, le flux parallèle JDK8 avec le nombre de pools de threads est plus pauvre. Le streaming parallèle implémente le code est simple et facile à comprendre, et nous n'avons pas besoin d'écrire plus pour des boucles. Nous pouvons compléter toutes les méthodes de parallèle et la quantité de code est considérablement réduite. En fait, la couche sous-jacente de streaming parallèle est toujours le cadre de fourche / jointure, qui nous oblige à utiliser de manière flexible diverses technologies pendant le processus de développement pour distinguer les avantages et les inconvénients de diverses technologies, afin de mieux nous servir.