1. Introducción de múltiples subprocesos
En la programación, no podemos evitar problemas de programación de múltiples subprocesos, porque se requiere un procesamiento concurrente en la mayoría de los sistemas comerciales. Si se encuentra en escenarios concurrentes, la múltiples subprocesos es muy importante. Además, durante nuestra entrevista, el entrevistador generalmente nos hace preguntas sobre múltiples subprocesos, como: ¿Cómo crear un hilo? Por lo general, respondemos de esta manera, hay dos métodos principales. El primero es: heredar la clase de subprocesos y reescribir el método de ejecución; El segundo es: implementar la interfaz ejecutable y reescribir el método de ejecución. Luego, el entrevistador definitivamente preguntará cuáles son las ventajas y desventajas de estos dos métodos. No importa qué, llegaremos a una conclusión, es decir, la segunda forma de uso, porque orientado a los objetos defiende menos herencia e intenta usar combinaciones tanto como sea posible.
En este momento, también podemos pensar qué hacer si queremos obtener el valor de retorno de los hilos múltiples. Según el conocimiento que hemos aprendido más, pensaremos en implementar la interfaz invocable y reescribir el método de llamada. ¿Cómo se usan tantos hilos en proyectos reales? ¿Cuántas maneras tienen?
Primero, echemos un vistazo a un ejemplo:
Este es un método simple para crear múltiples hechos, lo cual es fácil de entender. En el ejemplo, de acuerdo con diferentes escenarios comerciales, podemos pasar diferentes parámetros en Thread () para implementar diferentes lógicas comerciales. Sin embargo, el problema expuesto por este método para crear múltiples hilos es crear hilos repetidamente, y debe ser destruido después de crear hilos. Si los requisitos para escenarios concurrentes son bajos, este método parece estar bien, pero en escenarios de alta concurrencia, este método no es posible, porque crear subprocesos es muy consumo de recursos. Entonces, según la experiencia, la forma correcta es utilizar la tecnología de piscinas de subprocesos. JDK proporciona una variedad de tipos de piscinas de hilos para que podamos elegir. Para métodos específicos, puede verificar la documentación JDK.
Lo que debemos tener en cuenta en este código es que los parámetros pasados representan el número de subprocesos que configuramos. ¿Es cuanto más mejor? Seguramente no. Porque al configurar el número de subprocesos, debemos considerar completamente el rendimiento del servidor. Si hay más configuraciones de subprocesos, el rendimiento del servidor puede no ser excelente. Por lo general, los cálculos completados por la máquina están determinados por el número de hilos. Cuando el número de hilos alcanza el pico, no se puede realizar el cálculo. Si es la lógica comercial que consume CPU (más cálculos), el número de hilos y núcleos alcanzará su pico. Si es la lógica comercial que consume E/S (bases de datos operativas, cargando archivos, descarga, etc.), cuantos más hilos, más hilos, ayudará a mejorar el rendimiento en cierto sentido.
Otra fórmula para establecer el número de hilos:
Y = n*((a+b)/a), donde n: el número de núcleos de CPU, A: el tiempo de cálculo del programa cuando se ejecuta el hilo, B: el tiempo de bloqueo del programa cuando se ejecuta el hilo. Con esta fórmula, la configuración del conteo de subprocesos del grupo de subprocesos estará limitada, y podemos configurarla de manera flexible de acuerdo con la situación real de la máquina.
2. Comparación de optimización y rendimiento multiproceso
La tecnología de hilo se utilizó en proyectos recientes, y encontré muchos problemas durante el uso. Aprovechando la popularidad, resolveré las comparaciones de rendimiento de varios marcos de subprocesos múltiples. Los que hemos dominado se dividen aproximadamente en tres tipos: el primer tipo: Threadpool (grupo de hilos) + CountdownLatch (contador de programa), el segundo tipo: marco de horquilla/unión y el tercer tipo de flujo paralelo JDK8. Aquí hay un resumen comparativo del rendimiento múltiple de estos métodos.
Primero, suponga un escenario comercial donde se generan múltiples objetos de archivo en la memoria. Aquí, 30,000 hilos de sueño se determinan tentativamente para simular el procesamiento de negocios de la lógica de negocios para comparar el rendimiento múltiple de estos métodos.
1) Un solo roscado
Este método es muy simple, pero el programa lleva mucho tiempo durante el procesamiento y se usará durante mucho tiempo, porque cada hilo está esperando que el hilo actual se ejecute antes de que se ejecute. Tiene poco que ver con múltiples subprocesos, por lo que la eficiencia es muy baja.
Primero cree el objeto de archivo, el código es el siguiente:
public class FileInfo {private String FileName; // Nombre de archivo String String FileType de archivo; // Tipo de archivo String private String filesize; // Tamaño de archivo String private Filemd5; // MD5 Código Privado String String FileNversionNo; // File Número de versión Public FileInfo () {super (); } public FileInfo (String FileName, String FileType, String filesize, string filemd5, string fileVersionNo) {super (); this.FileName = FileName; 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 (cadena filemd5) {this.filemd5 = filemd5; } public String getFileVersionNo () {return FileVersionNo; } public void setFileVersionNo (String fileVersionNo) {this.fileVersionNo = fileVersionNo; }Luego, simule el procesamiento comercial, cree 30,000 objetos de archivo, el hilo duerme para 1 ms y establece 1000 ms antes, y descubre que el tiempo es muy largo y todo el eclipse está atascado, así que cambie el tiempo a 1 m.
Prueba de clase pública {Lista estática privada <FileInfo> fileList = new ArrayList <FileInfo> (); public static void main (string [] args) lanza interruptedException {createFileInfo (); Long Starttime = System.CurrentTimemillis (); para (fileInfo fi: filelist) {thread.sleep (1); } Long EndTime = System.CurrentTimemillis (); System.out.println ("Un solo hilo consumo de tiempo:"+(EndTime-starttime)+"MS"); } private static void createFileInfo () {for (int i = 0; i <30000; i ++) {filelist.add (nueva fileInfo ("foto frontal de la tarjeta de identificación", "jpg", "101522", "md5"+i, "1")); }}}Los resultados de la prueba son los siguientes:
Se puede ver que generar 30,000 objetos de archivo lleva mucho tiempo, casi 1 minuto, y la eficiencia es relativamente baja.
2) Threadpool (grupo de hilos) +CountdownLatch (contador de programa)
Como su nombre indica, CountdownLatch es un contador de hilo. Su proceso de ejecución es el siguiente: primero, el método Await () se llama en el hilo principal, y el hilo principal está bloqueado, y luego el contador del programa se pasa al objeto de subproceso como un parámetro. Finalmente, después de que cada hilo finalice la ejecución de la tarea, se llama al método Countdown () para indicar la finalización de la tarea. Después de que CountDown () se ejecute varias veces, el hilo principal espera () no será válido. El proceso de implementación es el siguiente:
public class test2 {private static ejecutorservice ejecutor = ejecutors.newfixedthreadpool (100); privado static CountdownLatch CountdownLatch = new CountdownLatch (100); Lista estática privada <FileInfo> fileList = new ArrayList <FileInfo> (); Lista estática privada <List <FileInfo>> list = new ArrayList <> (); public static void main (string [] args) lanza interruptedException {createFileInfo (); addlist (); Long Starttime = System.CurrentTimemillis (); int i = 0; para (List <FileInfo> fi: list) {Ejecutor.submit (new Filerunnable (CountDownLatch, Fi, I)); i ++; } CountdownLatch.AWait (); Long Time = System.CurrentTimemillis (); ejecutor.shutdown (); System.out.println (i+"Los hilos toman tiempo:"+(endtime-starttime)+"ms"); } private static void createFileInfo () {for (int i = 0; i <30000; i ++) {filelist.add (nuevo fileInfo ("foto de tarjeta de identificación frontal", "jpg", "101522", "md5"+i, "1")); }} private static void addList () {for (int i = 0; i <100; i ++) {list.add (fileList); }}}Clase de Filerunnable:
/** * Procesamiento multithreaded * @author wangsj * * @param <t> */public class FilerUnnable <T> implementa runnable {private CountdownLatch CountdownLatch; Lista de lista privada <t>; privado int i; public Filerunnable (CountDownLatch CountdownLatch, List <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 (); }}}Los resultados de la prueba son los siguientes:
3) Marco de bifurcación/unión
JDK comenzó con la versión 7, y apareció el marco de la bifurcación/unión. Desde una perspectiva literal, Fork se divide y se une es la fusión, por lo que la idea de este marco es. Divida la tarea a través de la bifurcación y luego únase para fusionar los resultados después de que los caracteres divididos se ejecutan y resuman. Por ejemplo, queremos calcular varios números que se agregan continuamente, 2+4+5+7 =? , ¿Cómo usamos el marco de la bifurcación/unión para completarlo? La idea es dividir las tareas moleculares. Podemos dividir esta operación en dos subtareas, una calcula 2+4 y la otra calcula 5+7. Este es el proceso de bifurcación. Después de completar el cálculo, se resumen los resultados del cálculo de estas dos subtareas y se obtiene la suma. Este es el proceso de unión.
Idea de ejecución del marco de la bifurcación/unión: Primero, divida las tareas y use la clase de la horquilla para dividir grandes tareas en varias subtareas. Este proceso de segmentación debe determinarse de acuerdo con la situación real hasta que las tareas divididas sean lo suficientemente pequeñas. Luego, la clase de unión ejecuta la tarea, y las subtareas divididas están en diferentes colas. Varios hilos obtienen tareas de la cola y los ejecutan. Los resultados de la ejecución se colocan en una cola separada. Finalmente, se inicia el hilo, los resultados se obtienen en la cola y los resultados se fusionan.
Se utilizan varias clases para usar el marco de la horquilla/unión. Para el uso de la clase, puede consultar la API JDK. Usando este marco, debe heredar la clase ForkJoTask. Por lo general, solo necesita heredar su subclase recursiva o recursiva. RecursiveTask se usa para escenas con resultados de retorno, y RecursIVEaction se usa para escenas sin resultados de retorno. La ejecución de ForkJoTask requiere la ejecución de Forkjoinpool, que se utiliza para mantener las subtareas divididas agregadas a diferentes colas de tareas.
Aquí está el código de implementación:
public class test3 {private static list <StilInfo> fileList = new ArrayList <FileInfo> (); // private static forkjoinpool forkjoinpool = new forkjoInpool (100); // trabajo estático privado <FileInfo> Job = nuevo trabajo <> (filelist.size ()/100, filelista); public static void main (string [] args) {createFileInfo (); Long Starttime = System.CurrentTimemillis (); Forkjoinpool Forkjoinpool = nuevo Forkjoinpool (100); // divide el trabajo de la tarea <fileInfo> Job = nuevo trabajo <> (filelist.size ()/100, filelist); // Enviar la tarea y devolver el resultado FORKJoCTASK <Integer> fjtresult = forkjoinpool.submit (trabajo); // Bloquear while (! Job.isdone ()) {System.out.println ("¡Tarea completa!"); } Long EndTime = System.CurrentTimemillis (); System.out.println ("Fork/Join Framework Consuming:"+(EndTime-starttime)+"MS"); } private static void createFileInfo () {for (int i = 0; i <30000; i ++) {filelist.add (nuevo fileInfo ("foto de tarjeta de identificación frontal", "jpg", "101522", "md5"+i, "1")); }}}/** * Ejecutar la clase de tareas * @author wangsj * */public class Job <T> extiende recursiveTask <integer> {private static final long SerialVersionUid = 1l; privado int count; Lista privada <t> joblista; trabajo público (int count, list <t> joblist) {super (); this.count = Count; this.Joblist = joblist; } /*** Ejecute la tarea, similar al método Ejecutar que implementa la interfaz runnable* /@Override proteged Integer Compute () {// divide la tarea if (boblist.size () <= count) {execUteJob (); return joblist.size (); } else {// Continúe creando la tarea hasta que pueda descomponerse y ejecutar la lista <recursiveTask <long>> fork = new LinkedList <recursiveTask <long>> (); // divide la tarea nucleica, aquí el método de dicotomía se usa int countJob = joblist.size ()/2; List <T> LeftList = Joblist.Sublist (0, CountJob); List <t> rightlist = joblist.sublist (countJob, joblist.size ()); // Asignar tareas Job LeftJob = New Job <> (Count, LeftList); Job RightJob = New Job <> (Count, Rightlist); // Ejecutar la tarea LeftJob.Fork (); rightJob.fork (); return integer.ParseInt (LeftJob.Join (). ToString ()) +Integer.ParseInt (RightJob.Join (). ToString ()); }} / *** Ejecutar el método de tarea* / private void ejecututeJob () {for (t trabajo: boblist) {try {thread.sleep (1); } catch (InterruptedException e) {E.PrintStackTrace (); }}}Los resultados de la prueba son los siguientes:
4) JDK8 transmisión paralela
El flujo paralelo es una de las nuevas características de JDK8. La idea es convertir una corriente ejecutada secuencialmente en un flujo concurrente, que se implementa llamando al método paralelo (). El flujo paralelo divide una secuencia en múltiples bloques de datos, utiliza diferentes hilos para procesar las secuencias de diferentes bloques de datos y finalmente fusiona los resultados de procesamiento de cada bloque de flujo de datos, similar al marco de la horquilla/unión.
La transmisión paralela utiliza el Public Thread Pool Forkjoinpool de forma predeterminada. El número de subprocesos es el valor predeterminado utilizado. Según el número de núcleos de la máquina, podemos ajustar el tamaño de los hilos de manera apropiada. Ajustar el número de hilos se logra de las siguientes maneras.
System.SetProperty ("java.util.concurrent.forkjoinpool.common.parallelism", "100");El siguiente es el proceso de implementación del código, que es muy simple:
Public Class Test4 {Lista estática privada <FileInfo> fileList = new ArrayList <FileInfo> (); public static void main (string [] args) {// system.setProperty ("java.util.concurrent.forkoinpool.common.parallelism", "100"); createFileInfo (); Long Starttime = System.CurrentTimemillis (); filelist.parallelStream (). foreach (e -> {try {hild.sleep (1);} capt (interruptedException f) {f.printstacktrace ();}}); Long Time = System.CurrentTimemillis (); System.out.println ("JDK8 Tiempo de transmisión paralelo:"+(Endtime-starttime)++"ms");} private static void createFileInfo () {for (int i = 0; i <30000; i ++) {fileList.Add (nuevo fileinfo ("foto de frontal de carta "," jpg "," 101522 "," md5 "+i," 1 ")); }}}La siguiente es la prueba. El número de grupos de subprocesos no está configurado por primera vez. Se utiliza el valor predeterminado. Los resultados de la prueba son los siguientes:
Vimos que el resultado no es muy ideal y lleva mucho tiempo. A continuación, establezca el número de grupos de subprocesos, es decir, agregue el siguiente código:
System.SetProperty ("java.util.concurrent.forkjoinpool.common.parallelism", "100");Luego se llevó a cabo la prueba, y los resultados fueron los siguientes:
Esta vez lleva menos tiempo y es ideal.
3. Resumen
Para resumir las situaciones anteriores, utilizando un solo hilo como referencia, el más largo que requiere tiempo es el marco nativo de la bifurcación/unión. Aunque el número de grupos de subprocesos está configurado aquí, el flujo paralelo JDK8 con el número de grupos de subprocesos es más pobre. Implementos de transmisión paralela El código es simple y fácil de entender, y no necesitamos escribir más para bucles. Podemos completar todos los métodos paralelstream, y la cantidad de código se reduce considerablemente. De hecho, la capa subyacente de transmisión paralela sigue siendo el marco de la horquilla/unión, lo que requiere que usemos de manera flexible varias tecnologías durante el proceso de desarrollo para distinguir las ventajas y desventajas de varias tecnologías, para que nos sirvan mejor.