1. Multi-Threading-Einführung
Bei der Programmierung können wir keine Multi-Threading-Programmierungsprobleme vermeiden, da in den meisten Geschäftssystemen eine gleichzeitige Verarbeitung erforderlich ist. Wenn es in gleichzeitigen Szenarien ist, ist Multi-Threading sehr wichtig. Während unseres Interviews stellt uns der Interviewer in der Regel Fragen zum Multi-Threading, z. B.: Wie erstellt man einen Thread? Wir antworten normalerweise auf diese Weise, es gibt zwei Hauptmethoden. Das erste ist: Erben Sie die Thread -Klasse und schreiben Sie die Run -Methode neu; Die zweite ist: Implementieren Sie die Runnable -Schnittstelle und schreiben Sie die Run -Methode neu. Dann wird der Interviewer definitiv fragen, was die Vor- und Nachteile dieser beiden Methoden sind. Egal was, wir werden zu einer Schlussfolgerung kommen, dh die zweite Art der Verwendung, da objektorientierte weniger Vererbung einbringt und versuchen, Kombinationen so weit wie möglich zu verwenden.
Zu diesem Zeitpunkt können wir auch darüber nachdenken, was zu tun ist, wenn wir den Rückgabewert von Multi-Threads erhalten möchten? Basierend auf dem Wissen, das wir mehr gelernt haben, werden wir uns über die Implementierung der Callable -Schnittstelle vorstellen und die Anrufmethode neu schreiben. Wie werden so viele Threads in tatsächlichen Projekten verwendet? Wie viele Möglichkeiten haben sie?
Schauen wir uns zunächst ein Beispiel an:
Dies ist eine einfache Methode, um Multi-Threads zu erstellen, was leicht zu verstehen ist. Im Beispiel können wir je nach Geschäftsszenarien verschiedene Parameter in Thread () übergeben, um eine unterschiedliche Geschäftslogik zu implementieren. Das Problem, das durch diese Methode zum Erstellen von Multi-Threads aufgedeckt wird, besteht jedoch darin, wiederholt Threads zu erstellen, und es muss nach dem Erstellen von Threads zerstört werden. Wenn die Anforderungen an gleichzeitige Szenarien niedrig sind, scheint diese Methode in Ordnung zu sein, aber in hohen Parallelitätsszenarien ist diese Methode nicht möglich, da das Erstellen von Threads sehr ressourcenkonsumend ist. Nach Erfahrung besteht die richtige Art und Weise, die Thread -Pool -Technologie zu verwenden. JDK bietet eine Vielzahl von Thread -Pooltypen für uns zur Auswahl. Für bestimmte Methoden können Sie die JDK -Dokumentation überprüfen.
Was wir in diesem Code beachten müssen, ist, dass die darin übergebenen Parameter die Anzahl der von uns konfigurierten Threads darstellen. Ist desto mehr desto besser? Sicher nicht. Denn bei der Konfiguration der Anzahl der Threads müssen wir die Leistung des Servers vollständig berücksichtigen. Wenn mehr Threadkonfigurationen vorhanden sind, ist die Leistung des Servers möglicherweise nicht ausgezeichnet. Normalerweise werden die von der Maschine durchgeführten Berechnungen durch die Anzahl der Threads bestimmt. Wenn die Anzahl der Fäden den Peak erreicht, kann die Berechnung nicht durchgeführt werden. Wenn es sich um die Geschäftslogik handelt, die CPU verbraucht (mehr Berechnungen), erreicht die Anzahl der Fäden und Kerne ihren Höhepunkt. Wenn es sich um die Geschäftslogik handelt, die E/A (Betriebsdatenbanken, Hochladen von Dateien, Herunterladen usw.), je mehr Threads, mehr Threads, die Leistung in gewissem Sinne verbessert.
Eine andere Formel, um die Anzahl der Threads festzulegen:
Y = n*((a+b)/a), wobei n: Die Anzahl der CPU -Kerne, a: die Berechnung des Programms, wenn der Thread ausgeführt wird, b: die Blockierungszeit des Programms, wenn der Thread ausgeführt wird. Mit dieser Formel wird die Thread -Count -Konfiguration des Thread -Pools eingeschränkt, und wir können sie flexibel entsprechend der tatsächlichen Situation der Maschine konfigurieren.
2. Multithread -Optimierung und Leistungsvergleich
Die Threading -Technologie wurde in jüngsten Projekten eingesetzt, und ich hatte während des Gebrauchs viel Ärger. Wenn ich die Popularität nutze, werde ich die Leistungsvergleiche mehrerer Multi-Thread-Frameworks sortieren. Diejenigen, die wir gemeistert haben, sind grob in drei Typen unterteilt: der erste Typ: Threadpool (Thread Pool) + Countdownlatch (Programmzähler), der zweite Typ: Fork/Join -Framework und der dritte Typ von JDK8 Parallel Stream. Hier finden Sie eine vergleichende Zusammenfassung der Multi-Threading-Leistung dieser Methoden.
Nehmen Sie zunächst ein Geschäftsszenario an, in dem mehrere Dateiobjekte im Speicher generiert werden. Hier wird 30.000 Thread Sleep vorläufig festgelegt, um die Geschäftslogik der Geschäftsbearbeitung zu simulieren, um die Multi-Threading-Leistung dieser Methoden zu vergleichen.
1) Einzelfaden
Diese Methode ist sehr einfach, aber das Programm ist während der Verarbeitung sehr zeitaufwändig und wird für lange Zeit verwendet, da jeder Thread darauf wartet, dass der aktuelle Thread ausgeführt wird, bevor er ausgeführt wird. Es hat wenig mit Multi-Threads zu tun, daher ist die Effizienz sehr niedrig.
Erstellen Sie zuerst das Dateiobjekt, der Code ist wie folgt:
public class FileInfo {private Zeichenfolge Dateiname; // Dateiname private Zeichenfolge Filetype; // Dateityp private Zeichenfolge FileSize; // Dateigröße private Zeichenfolge FilEMD5; // Md5 Code private String -FileversionNo; // Dateiversionsnummer öffentlich FileInfo () {Super (); } public FileInfo (String -Dateiname, String Filetype, String FileSized, String FilEMD5, String FileVesionNo) {Super (); this.FileName = Dateiname; this.filetype = Filetype; this.FileSize = fileSize; this.filemd5 = filemd5; this.FileVersionno = FileVesionNO; } public String getFileName () {return Dateiname; } public void setFileName (String Dateiname) {this.FileName = Dateiname; } public String getFileType () {return Filetype; } public void setFileType (String Filetype) {this.filetype = Filetype; } public String getFileSize () {ideSize return; } public void setFileSize (String FileSize) {this.FileSize = ideSize; } public String getFilemd5 () {return fileMd5; } public void setFilemd5 (String fileMd5) {this.filemd5 = filemd5; } public String getFileversionNo () {return FileVersionNo; } public void setFileVersionNO (String FileVesionNo) {this.FileVersionNo = FileVesionNO; }Simulieren Sie dann die Geschäftsabwicklung, erstellen Sie 30.000 Dateiobjekte, den Thread Sleeps für 1 ms und legt vor 1000 ms fest und stellt fest, dass die Zeit sehr lang ist und die gesamte Sonnenfinsternis steckt. Ändern Sie also die Zeit auf 1 ms.
public class test {private statische Liste <DileInfo> filelist = new ArrayList <DateiInfo> (); public static void main (String [] args) löst InterruptedException {createFileInfo () aus; Long start time = system.currenttimemillis (); für (FileInfo fi: filelist) {thread.sleep (1); } Long EndTime = System.currentTimemillis (); System.out.println ("Einzelphread zeitaufwändig:"+(Endime-StartTime)+"MS"); } private static void createFileInfo () {für (int i = 0; i <30000; i ++) {filelist.add (neuer FileInfo ("Frontfoto der ID -Karte", "JPG", "101522", "MD5"+i, "1"); }}}Die Testergebnisse sind wie folgt:
Es ist ersichtlich, dass die Erzeugung von 30.000 Dateiobjekten lange, fast 1 Minute dauert und die Effizienz relativ niedrig ist.
2) Threadpool (Thread Pool) +Countdownlatch (Programmzähler)
Wie der Name schon sagt, ist Countdownlatch ein Threadschalter. Sein Ausführungsprozess ist wie folgt: Zuerst wird die Await () -Methode im Haupt -Thread aufgerufen und der Haupt -Thread blockiert, und dann wird der Programmschalter als Parameter an das Thread -Objekt übergeben. Nachdem jeder Thread die Ausführung der Aufgabe abgeschlossen hat, wird die Methode Countdown () aufgerufen, um die Ausführung der Aufgabe anzuzeigen. Nachdem Countdown () mehrmals ausgeführt wurde, erwartet der Haupt -Thread () ungültig. Der Implementierungsprozess ist wie folgt:
public class test2 {private static ExecutorService Executor = Executors.NewFixedThreadpool (100); private statische Countdownlatch Countdownlatch = new Countdownlatch (100); private statische Liste <DileInfo> filelist = new ArrayList <DileInfo> (); private statische Liste <list <FileInfo >> list = new ArrayList <> (); public static void main (String [] args) löst InterruptedException {createFileInfo () aus; addlist (); Long start time = system.currenttimemillis (); int i = 0; für (list <DateiInfo> fi: list) {executor.submit (neues filerunnable (Countdownlatch, fi, i)); i ++; } Countdownlatch.await (); Long EndTime = System.currentTimemillis (); Executor.Shutdown (); System.out.println (i+"Threads brauchen Zeit:"+(Endzeitstart)+"MS"); } private static void createFileInfo () {für (int i = 0; i <30000; i ++) {filelist.add (neuer FileInfo ("Front -ID -Kartenfoto", "JPG", "101522", "Md5"+i, "1"); }} private static void addlist () {für (int i = 0; i <100; i ++) {list.add (filelist); }}}Filerunnable Klasse:
/** * Multithread -Verarbeitung * @Author Wangsj * * @param <t> */öffentliche Klasse Filerunnable <T> Implements Runnable {private Countdownlatch -Countdownlatch; private Liste <T> Liste; privat int i; public fileRunnable (Countdownlatch Countdownlatch, Liste <T> Liste, int i) {Super (); this.countdownlatch = Countdownlatch; this.list = list; this.i = i; } @Override public void run () {für (t t: list) {try {thread.sleep (1); } catch (interruptedException e) {e.printstacktrace (); } Countdownlatch.countdown (); }}}Die Testergebnisse sind wie folgt:
3) Fork/Join -Framework
JDK begann mit Version 7 und das Fork/Join -Framework erschien. Aus wörtlicher Sicht spaltet Gabel und der Zusammenschluss, sodass die Idee dieses Rahmens ist. Teilen Sie die Aufgabe durch Gabel auf und verbinden Sie dann die Ergebnisse, nachdem die geteilten Zeichen ausgeführt und zusammengefasst wurden. Zum Beispiel möchten wir mehrere Zahlen berechnen, die kontinuierlich hinzugefügt werden, 2+4+5+7 =? Wie verwenden wir das Fork/Join -Framework, um es zu vervollständigen? Die Idee ist, die molekularen Aufgaben zu teilen. Wir können diesen Vorgang in zwei Unteraufgaben aufteilen, einer berechnet 2+4 und der andere berechnet 5+7. Dies ist der Prozess der Gabel. Nach Abschluss der Berechnung werden die Ergebnisse der Berechnung dieser beiden Unteraufgaben zusammengefasst und die Summe erhalten. Dies ist der Prozess des Join.
Fork/Join -Framework -Ausführung Idee: Erstens teilen Sie Aufgaben und verwenden Sie die Gabelklasse, um große Aufgaben in mehrere Unteraufgaben zu unterteilen. Dieser Segmentierungsprozess muss gemäß der tatsächlichen Situation bestimmt werden, bis die geteilten Aufgaben klein genug sind. Dann führt die Join -Klasse die Aufgabe aus, und die geteilten Unteraufgaben befinden sich in verschiedenen Warteschlangen. Mehrere Threads erhalten Aufgaben aus der Warteschlange und führen sie aus. Die Ausführungsergebnisse werden in eine separate Warteschlange gestellt. Schließlich wird der Thread gestartet, die Ergebnisse werden in der Warteschlange erzielt und die Ergebnisse werden zusammengeführt.
Mehrere Klassen werden verwendet, um das Fork/Join -Framework zu verwenden. Für die Verwendung der Klasse können Sie sich auf die JDK -API beziehen. Mit diesem Framework müssen Sie die Forkjointask -Klasse erben. Normalerweise müssen Sie nur die Unterklasse Recursivetask oder Recursiveaction erben. Recursivetask wird für Szenen mit Rückgabergebnissen verwendet, und Recursiveaction wird für Szenen ohne Rückgabergebnisse verwendet. Die Ausführung von Forkjointask erfordert die Ausführung von Forkjoinpool, mit der die geteilten Unteraufgaben zu verschiedenen Aufgabenwarteschlangen aufrechterhalten werden.
Hier ist der Implementierungscode:
public class Test3 { private static List<FileInfo> fileList= new ArrayList<FileInfo>();// private static ForkJoinPool forkJoinPool=new ForkJoinPool(100);// private static Job<FileInfo> job=new Job<>(fileList.size()/100, fileList); public static void main (String [] args) {createFileInfo (); Long start time = system.currenttimemillis (); Forkjoinpool forkjoinpool = new forkjoinpool (100); // Task Job <Dateinfo> Job = New Job <> (filelist.size ()/100, filelist) teilen; // die Aufgabe einreichen und das Ergebnis forkjointask <Gearner> fjtresult = forkjoinpool.submit (Job) zurückgeben; // block whit (! Job.isdone ()) {System.out.println ("Aufgabe abgeschlossen!"); } Long EndTime = System.currentTimemillis (); System.out.println ("Fork/Join Framework zeitaufwändig:"+(Endzeitstart)+"MS"); } private static void createFileInfo () {für (int i = 0; i <30000; i ++) {filelist.add (neuer FileInfo ("Front -ID -Kartenfoto", "JPG", "101522", "Md5"+i, "1"); }}}/** * Taskklasse ausführen * @Author Wangsj * */public class Job <T> Erweitert Recursivetask <Grapier> {private statische endgültige long serialversionuid = 1l; private Int Count; Private List <T> Joblist; public job (int count, list <t> joblist) {super (); this.count = count; this.joblist = jublist; } /*** Führen Sie die Aufgabe aus, ähnlich der Auslaufmethode, die die runnable Schnittstelle implementiert return junglist.size (); } else {// Erstellen Sie die Aufgabe weiter, bis sie zerlegt und ausgeführt werden kann. // Teilen Sie die Nucleisc -Aufgabe, hier wird die Dichotomie -Methode verwendet. LIST <T> linkslist = joblist.sublist (0, countjob); List <T> rightList = junglist.sublist (countjob, junglist.size ()); // Aufgaben zuweisen Job linksjob = neuer Job <> (count, linkslist); Job rightjob = neuer Job <> (count, rightlist); // Die Aufgabe linksjob.fork () ausführen; rightjob.fork (); return Integer.parseInt (linksjob.join (). toString ()) +Integer.ParseInt (rightjob.join (). toString ()); }} / *** Führen Sie die Task -Methode aus* / private void executejob () {für (t Job: junglist) {try {thread.sleep (1); } catch (interruptedException e) {e.printstacktrace (); }}}Die Testergebnisse sind wie folgt:
4) JDK8 Parallele Streaming
Der parallele Fluss ist eines der neuen Merkmale von JDK8. Die Idee ist, einen Stream, der nacheinander ausgeführt wurde, in einen gleichzeitigen Fluss zu verwandeln, der durch Aufrufen der Parallel () -Methode implementiert wird. Der parallele Fluss unterteilt einen Stream in mehrere Datenblöcke, verarbeitet verschiedene Threads die Streams verschiedener Datenblöcke und verschmilzt schließlich die Verarbeitungsergebnisse jedes Datenstromblocks, ähnlich dem Fork/Join -Framework.
Der parallele Stream verwendet standardmäßig den öffentlichen Thread Pool Forkjoinpool. Die Anzahl der Threads ist der verwendete Standardwert. Nach der Anzahl der Maschinenkerne können wir die Größe der Fäden angemessen anpassen. Das Anpassen der Anzahl der Fäden wird auf folgende Weise erreicht.
System.setProperty ("java.util.concurrent.forkjoinpool.common.Parallelism", "100");Das Folgende ist der Implementierungsprozess des Codes, der sehr einfach ist:
public class test4 {private statische Liste <DileInfo> filelist = new ArrayList <DateiInfo> (); public static void main (String [] args) {// system.setProperty ("java.util.concurrent.forkjoinpool.common.Parallelism", "100"); createFileInfo (); Long start time = system.currenttimemillis (); filelist.parallelStream (). foreach (e -> {try {thread.sleep (1);} catch (interruptedException f) {f.printstacktrace ();}}); Long EndTime = System.currentTimemillis (); System.out.println ("JDK8 Parallel Streaming-Zeit:"+(Endime-StartTime)+"ms");} private statische void createFileInfo () {für (int i = 0; i <30000; i ++) {filelist.add (new FileInfo (“Frontfoto von IDD von Idd von IDD von IDD von IDD von IDD von Idd von Idd von Idd von Idd. Karte "," jpg "," 101522 "," md5 "+i," 1 "); }}}Das Folgende ist der Test. Die Anzahl der Threadpools wird zum ersten Mal nicht eingestellt. Der Standard wird verwendet. Die Testergebnisse sind wie folgt:
Wir haben gesehen, dass das Ergebnis nicht sehr ideal ist und lange dauert. Setzen Sie als nächstes die Anzahl der Threadpools, dh den folgenden Code hinzu:
System.setProperty ("java.util.concurrent.forkjoinpool.common.Parallelism", "100");Dann wurde der Test durchgeführt, und die Ergebnisse waren wie folgt:
Diesmal braucht es weniger Zeit und ist ideal.
3. Zusammenfassung
Um die obigen Situationen zusammenzufassen und einen einzelnen Thread als Referenz zu verwenden, ist das längste zeitaufwändige native Fork/Join-Framework. Obwohl hier die Anzahl der Threadpools konfiguriert ist, ist der parallele JDK8 -Stream mit der Anzahl der Threadpools ärmer. Parallele Streaming implementiert, dass der Code einfach und leicht zu verstehen ist, und wir müssen nicht zusätzliche für Schleifen schreiben. Wir können alle Parallelstream -Methoden vervollständigen, und die Menge an Code wird stark reduziert. Tatsächlich ist die zugrunde liegende Ebene des parallelen Streamings immer noch das Fork/Join -Framework, wodurch wir während des Entwicklungsprozesses flexibel verschiedene Technologien einsetzen müssen, um die Vor- und Nachteile verschiedener Technologien zu unterscheiden, um uns besser zu dienen.