El enfoque de este artículo está en los problemas de rendimiento de las aplicaciones multiproceso. Primero definiremos el rendimiento y la escalabilidad, y luego estudiaremos cuidadosamente la regla AMDAHL. En el siguiente contenido, examinaremos cómo usar diferentes métodos técnicos para reducir la competencia de bloqueo y cómo implementarlo con código.
1. Rendimiento
Todos sabemos que la Leading Multithread se puede usar para mejorar el rendimiento del programa, y la razón detrás de esto es que tenemos CPU múltiples o CPU múltiples. Cada núcleo de CPU puede completar las tareas por sí solo, por lo que irrumpir una gran tarea en una serie de pequeñas tareas que se pueden ejecutar independientemente entre sí puede mejorar el rendimiento general del programa. Puedes dar un ejemplo. Por ejemplo, hay un programa que cambia el tamaño de todas las imágenes en una carpeta en el disco duro, y la aplicación de tecnología de subprocesos múltiples puede mejorar su rendimiento. El uso de un solo enfoque roscado solo puede atravesar todos los archivos de imagen en secuencia y realizar modificaciones. Si nuestra CPU tiene múltiples núcleos, no hay duda de que solo puede usar uno de ellos. Usando múltiples subprocesos, podemos hacer que un hilo de productor escanee el sistema de archivos para agregar cada imagen a una cola, y luego usar múltiples subprocesos de trabajadores para realizar estas tareas. Si el número de subprocesos de trabajadores es el mismo que el número total de núcleos de CPU, podemos asegurarnos de que cada núcleo de CPU tenga trabajo que hacer hasta que se ejecuten todas las tareas.
Para otro programa que requiere más espera IO, el rendimiento general también se puede mejorar utilizando tecnología de subprocesos múltiples. Supongamos que queremos escribir un programa de este tipo que necesitamos rastrear todos los archivos HTML de un determinado sitio web y almacenarlos en el disco local. El programa puede comenzar desde una determinada página web, luego analizar todos los enlaces a este sitio web en esta página web y luego rastrear estos enlaces a su vez, para que se repita. Debido a que lleva un tiempo esperar desde el momento en que iniciamos una solicitud al sitio web remoto al tiempo que recibimos todos los datos de la página web, podemos entregar esta tarea a múltiples subprocesos para la ejecución. Deje que uno o un poco más de hilo analice la página HTML recibida y coloque el enlace encontrado en la cola, dejando a todos los demás hilos responsables de solicitar la página. A diferencia del ejemplo anterior, en este ejemplo, aún puede obtener mejoras de rendimiento incluso si usa más hilos que la cantidad de núcleos de CPU.
Los dos ejemplos anteriores nos dicen que el alto rendimiento es hacer tantas cosas como sea posible en una ventana de tiempo corto. Esta es, por supuesto, la explicación más clásica del término rendimiento. Pero al mismo tiempo, el uso de hilos también puede mejorar bien la velocidad de respuesta de nuestros programas. Imagine que tenemos una aplicación de interfaz gráfica de este tipo, con un cuadro de entrada arriba y un botón llamado "proceso" debajo del cuadro de entrada. Cuando el usuario presiona este botón, la aplicación debe volver a renderizar el estado del botón (el botón parece presionarse y vuelve a su estado original cuando se lanza el botón izquierdo del mouse) y comenzar a procesar la entrada del usuario. Si esta tarea lleva mucho tiempo procesar la entrada del usuario, un programa de un solo hilo no podrá continuar respondiendo a otras acciones de entrada del usuario, como el usuario haciendo clic en el evento del mouse o el puntero del mouse que mueve el evento transmitido desde el sistema operativo, etc. Las respuestas a estos eventos deben ser un hilo independiente para responder.
La escalabilidad significa que los programas tienen la capacidad de obtener un mayor rendimiento al agregar recursos informáticos. Imagine que necesitamos ajustar el tamaño de muchas imágenes, porque el número de núcleos de CPU de nuestra máquina es limitado, lo que aumenta el número de hilos no siempre mejora el rendimiento en consecuencia. Por el contrario, debido a que el programador debe ser responsable de la creación y el cierre de más hilos, también ocupará recursos de CPU, lo que puede reducir el rendimiento.
1.1 regla de amdahl
El párrafo anterior mencionó que en algunos casos, agregar recursos informáticos adicionales puede mejorar el rendimiento general del programa. Para calcular cuánta mejora del rendimiento podemos obtener cuando agregamos recursos adicionales, es necesario verificar qué partes del programa se ejecutan en serie (o sincrónicamente) y qué piezas se ejecutan en paralelo. Si cuantificamos la proporción de código que debe ejecutarse sincrónicamente a B (por ejemplo, el número de líneas de código que debe ejecutarse sincrónicamente) y registrar el número total de núcleos de la CPU como N, entonces, de acuerdo con la Ley AMDAHL, el límite superior de las mejoras de rendimiento que podemos obtener es:
Si N tiende al infinito, (1-B)/N converge a 0. Por lo tanto, podemos ignorar el valor de esta expresión, por lo que el recuento de bits de mejora del rendimiento converge a 1/B, donde B representa la proporción de código que debe ejecutarse sincrónicamente. Si B es igual a 0.5, significa que la mitad del código del programa no puede ejecutarse en paralelo, y el recíproco de 0.5 es 2, por lo que incluso si agregamos innumerables núcleos de CPU, obtenemos un máximo de 2x mejora del rendimiento. Supongamos que hemos modificado el programa ahora, y después de la modificación, solo 0.25 código debe ejecutarse sincrónicamente. Ahora 1/0.25 = 4 significa que si nuestro programa se ejecuta en hardware con una gran cantidad de CPU, será aproximadamente 4 veces más rápido que en el hardware de un solo núcleo.
Por otro lado, a través de la Ley AMDAHL, también podemos calcular la proporción del código de sincronización que el programa debe basarse en el objetivo de aceleración que queremos obtener. Si queremos lograr una aceleración 100 veces, y 1/100 = 0.01 significa que el número máximo de código que nuestro programa ejecuta sincrónicamente no puede exceder el 1%.
Para resumir la ley de AMDAHL, podemos ver que la mejora máxima del rendimiento que obtenemos al agregar una CPU adicional depende de qué tan pequeña la proporción del programa ejecute parte del código sincrónicamente. Aunque en realidad, no siempre es fácil calcular esta relación, y mucho menos enfrentar algunas aplicaciones de sistemas comerciales grandes, la ley de Amdahl nos da una inspiración importante, es decir, debemos considerar el código que debe ejecutarse sincrónicamente e intentar reducir esta parte del código.
1.2 Efecto sobre el rendimiento
Como el artículo escribe aquí, hemos hecho un punto en que agregar más hilos puede mejorar el rendimiento y la capacidad de respuesta del programa. Pero, por otro lado, no es fácil lograr estos beneficios, y también requiere algo de precio. El uso de hilos también afectará la mejora del rendimiento.
Primero, el primer impacto proviene del momento de la creación de hilos. Durante la creación de subprocesos, el JVM debe solicitar los recursos correspondientes del sistema operativo subyacente e inicializar la estructura de datos en el planificador para determinar el orden de los hilos de ejecución.
Si su número de hilos es el mismo que el número de núcleos de CPU, cada hilo se ejecutará en un núcleo para que no se interrumpan con frecuencia. Pero, de hecho, cuando su programa se está ejecutando, el sistema operativo también tendrá algunas de sus propias operaciones que la CPU debe procesar. Entonces, incluso en este caso, su hilo se interrumpirá y esperará a que el sistema operativo reanude su operación. Cuando su recuento de hilos excede el número de núcleos de CPU, la situación puede empeorar. En este caso, el programador de procesos del JVM interrumpirá ciertos subprocesos para permitir que otros subprocesos se ejecuten. Cuando se cambian los subprocesos, el estado actual del subproceso en ejecución debe guardar para que el estado de datos se pueda restaurar la próxima vez que se ejecute. No solo eso, el planificador también actualizará su propia estructura de datos internos, que también requiere ciclos de CPU. Todo esto significa que el cambio de contexto entre hilos consume recursos informáticos de CPU, lo que provoca una sobrecarga de rendimiento en comparación con la de un solo caso roscado.
Otra gastos generales traídos por programas multiproceso proviene de la protección de acceso sincrónico de datos compartidos. Podemos usar la palabra clave sincronizada para la protección de sincronización, o podemos usar la palabra clave volátil para compartir datos entre múltiples hilos. Si más de un hilo quiere acceder a una estructura de datos compartida, se producirá una contienda. En este momento, el JVM necesita decidir qué proceso es el primero y qué proceso está detrás. Si el hilo que se ejecuta no es el hilo actualmente en ejecución, se produce una conmutación de hilo. El hilo actual debe esperar hasta que adquiera con éxito el objeto de bloqueo. El JVM puede decidir cómo realizar esta "espera". Si el JVM espera ser más corto para adquirir con éxito el objeto bloqueado, el JVM puede usar métodos de espera agresivos, como tratar constantemente de adquirir el objeto bloqueado hasta que sea exitoso. En este caso, este método puede ser más eficiente, porque aún es más rápido comparar el cambio de contexto de proceso. Mover un hilo de espera de regreso a la cola de ejecución también traerá una sobrecarga adicional.
Por lo tanto, debemos hacer todo lo posible para evitar el cambio de contexto causado por la competencia de bloqueo. La siguiente sección explicará dos formas de reducir la ocurrencia de dicha competencia.
1.3 Competencia de bloqueo
Como se mencionó en la sección anterior, el acceso competitivo al bloqueo por dos o más subprocesos traerá una sobrecarga computacional adicional porque la competencia ocurre para obligar al planificador a ingresar a un estado de espera agresivo, o dejar que realice un estado de espera, causando dos interruptores de contexto. Hay algunos casos en los que las consecuencias de la competencia de bloqueo pueden ser mitigadas por:
1. Reduce el alcance de las cerraduras;
2. Reduzca la frecuencia de los bloqueos que deben adquirirse;
3. Intente usar operaciones de bloqueo optimistas compatibles con hardware en lugar de sincronizado;
4. Intenta usar sincronizado lo menos posible;
5. Reduce el uso de caché de objetos
1.3.1 Reducción del dominio de sincronización
Si el código mantiene el bloqueo por más de lo necesario, entonces se puede aplicar este primer método. Por lo general, podemos mover una o más líneas de código fuera del área de sincronización para reducir el tiempo que el hilo actual contiene el bloqueo. Cuanto menos código se ejecute en el área de sincronización, más temprano el hilo actual librará el bloqueo, permitiendo que otros hilos adquieran el bloqueo antes. Esto es consistente con la Ley AMDAHL, porque hacerlo reduce la cantidad de código que debe ejecutarse sincrónicamente.
Para una mejor comprensión, mire el siguiente código fuente:
Public Class ReduceLockDuration implementa Runnable {private static final int number_of_threads = 5; Mapa final estático privado <String, Integer> map = new Hashmap <String, Integer> (); public void run () {for (int i = 0; i <10000; i ++) {sincronizado (map) {uuid randomUuid = uuid.randomuuid (); Valor entero = integer.valueOf (42); Clave de cadena = RandomUuid.ToString (); map.put (clave, valor); } Thread.yield (); }} public static void main (string [] args) lanza interruptedException {hilo [] hilts = new Thread [number_of_threads]; for (int i = 0; i <número_of_threads; i ++) {hilos [i] = new Thread (new ReduceLockDuration ()); } Long StartMillis = System.CurrentTimemillis (); for (int i = 0; i <número_of_threads; i ++) {hilos [i] .Start (); } for (int i = 0; i <number_of_threads; i ++) {hilos [i] .Join (); } System.out.println ((System.CurrentTimemillis ()-StartMillis)+"MS"); }}En el ejemplo anterior, dejamos que cinco hilos compitan para acceder a la instancia de mapa compartido. Para que solo un hilo pueda acceder a la instancia del mapa al mismo tiempo, colocamos el funcionamiento de agregar clave/valor al mapa en el bloque de código protegido sincronizado. Cuando observamos cuidadosamente este código, podemos ver que las pocas oraciones de código que calculan la clave y el valor no necesitan ser ejecutadas sincrónicamente. La clave y el valor solo pertenecen al hilo que actualmente ejecuta este código. Solo es significativo para el hilo actual y no será modificado por otros hilos. Por lo tanto, podemos sacar estas oraciones fuera de la protección de sincronización. como sigue:
public void run () {for (int i = 0; i <10000; i ++) {uuid randomUuid = uuid.randomuuid (); Valor entero = integer.valueOf (42); Clave de cadena = RandomUuid.ToString (); sincronizado (map) {map.put (clave, valor); } Thread.yield (); }}El efecto de reducir el código de sincronización es medible. En mi máquina, el tiempo de ejecución de todo el programa se redujo de 420 ms a 370 ms. Eche un vistazo, solo mover tres líneas de código fuera del bloque de protección de sincronización puede reducir el tiempo de ejecución del programa en un 11%. El código Thread.yield () es para inducir la conmutación de contexto de subprocesos, porque este código le indicará al JVM que el hilo actual quiere entregar los recursos de cómputo utilizados actualmente para que otros hilos que esperan se ejecutarán. Esto también conducirá a una mayor competencia de bloqueo, porque si este no es el caso, un hilo ocupará un cierto núcleo más largo, reduciendo así el cambio de contexto de hilo.
1.3.2 Bloqueo dividido
Otra forma de reducir la competencia de bloqueo es difundir un bloque de código protegido con bloqueo en varios bloques de protección más pequeños. Este método funcionará si usa un bloqueo en su programa para proteger múltiples objetos diferentes. Supongamos que queremos contar algunos datos a través de un programa e implementar una clase de conteo simple para mantener múltiples indicadores estadísticos diferentes y representarlos con una variable de recuento básico (tipo largo). Debido a que nuestro programa es multiproceso, necesitamos proteger sincrónicamente las operaciones que acceden a estas variables, porque estas acciones provienen de diferentes hilos. La forma más fácil de lograr esto es agregar la palabra clave sincronizada a cada función que acceda a estas variables.
Class de clase estática pública El contador de implementos de implementos {private long CustomerCount = 0; shippingco de envío largo privado = 0; public sincronizado void incremementCustomer () {CustomerCount ++; } public sincronizado void incremementShipping () {ShippingCount ++; } public sincronizado Long getCustomerCount () {return CustomerCount; } public sincronizado Long GetShippingCount () {return ShippingCount; }}Esto significa que cada modificación de estas variables causará bloqueo a otras instancias de mostrador. Si otros hilos quieren llamar al método de incremento en otra variable diferente, solo pueden esperar a que el hilo anterior libere el control de bloqueo antes de tener la oportunidad de completarlo. En este caso, el uso de una protección sincronizada separada para cada variable diferente mejorará la eficiencia de la ejecución.
Public static class CounterSaratelock implementa contador {objeto final estático privado customerlock = new Object (); objeto final estático privado shippinglock = new Object (); Private Long CustomerCount = 0; shippingco de envío largo privado = 0; public void incrementCustomer () {Synchronized (CustomerLock) {CustomerCount ++; }} public void incremementShipping () {Synchronized (ShippingLock) {ShippingCount ++; }} public Long Long getCustomerCount () {SynChronized (CustomerLock) {return CustomerCount; }} public Long Long GetShippingCount () {SynChronized (ShippingLock) {return ShippingCount; }}}Esta implementación introduce un objeto sincronizado separado para cada métrica de recuento. Por lo tanto, cuando un hilo quiere aumentar el recuento de clientes, debe esperar a otro hilo que esté aumentando el recuento de clientes para completar, en lugar de esperar otro hilo que aumente el recuento de envíos para completar.
Usando las siguientes clases, podemos calcular fácilmente las mejoras de rendimiento traídas por bloqueos divididos.
implementos de bloqueo de clase pública runnable {private static final int number_of_threads = 5; mostrador privado; contador de interfaz pública {void incremementCustomer (); incremento nulo (); Long getCustomercount (); Long GetShippingCount (); } Public static class CountonElock implementa contador {...} public static class static CounterSparatelock implementa contador {...} public sportsitting (contador de contador) {this.counter = contador; } public void run () {for (int i = 0; i <100000; i ++) {if (ThreadLocalRandom.Current (). nextBoolean ()) {contunt.incrementCustomer (); } else {Counter.IrcrementShipping (); }}} public static void main (string [] args) arroja interruptedException {hilo [] hilos = new Thread [number_of_threads]; Contador de contador = new Countonelock (); for (int i = 0; i <number_of_threads; i ++) {hilos [i] = new Thread (new Locksplitting (contador)); } Long StartMillis = System.CurrentTimemillis (); for (int i = 0; i <número_of_threads; i ++) {hilos [i] .Start (); } for (int i = 0; i <number_of_threads; i ++) {hilos [i] .Join (); } System.out.println ((System.CurrentTimemillis () - StartMillis) + "MS"); }}En mi máquina, el método de implementación de un solo bloqueo toma un promedio de 56 ms, y la implementación de dos bloqueos separados es de 38 ms. El tiempo que lleva tiempo se reduce en aproximadamente un 32%.
Otra forma de mejorar es que incluso podemos ir más allá para proteger la lectura y escribir con diferentes cerraduras. La clase de mostrador original proporciona métodos para la lectura y la redacción de los indicadores de conteo respectivamente. Sin embargo, de hecho, las operaciones de lectura no requieren protección de sincronización. Podemos estar seguros de que múltiples subprocesos pueden leer el valor del indicador actual en paralelo. Al mismo tiempo, las operaciones de escritura deben protegerse sincrónicamente. El paquete java.util.concurrent proporciona una implementación de la interfaz ReadWriteLock, que puede lograr fácilmente esta distinción.
La implementación ReentRantReadWriteLock mantiene dos cerraduras diferentes, uno protege la operación de lectura y el otro protege la operación de escritura. Ambas cerraduras tienen operaciones para adquirir y liberar cerraduras. Un bloqueo de escritura solo se puede obtener con éxito cuando nadie adquiere un bloqueo de lectura. Por el contrario, siempre que no se adquiera el bloqueo de escritura, el bloqueo de lectura puede ser adquirido mediante múltiples hilos al mismo tiempo. Para demostrar este enfoque, la siguiente clase de contador usa ReadWriteLock, de la siguiente manera:
public static class CounterReadWriteLock implementa contador {privado final reentRantReadWriteLock CustomerLock = new ReEntantReadWriteLock (); Lock final privado CustomerwriteLock = CustomerLock.WriteLock (); Lock final privado CustomerReadlock = CustomerLock.Readlock (); REENTRANTREADWRITELOCK FINAL PRIVADO SHIENGLOCK = new ReEntRantReadWriteLock (); Lock final privado ShiptewriteLock = ShippingLock.WriteLock (); Lock final privado ShipteReadlock = shippinglock.readlock (); Private Long CustomerCount = 0; shippingco de envío largo privado = 0; public void incrementCustomer () {customerWriteLock.lock (); CustomerCount ++; CustomerWriteLock.unlock (); } public void incremementShipping () {shippingwriteLock.lock (); ShippingCount ++; ShippingWriteLock.unlock (); } public Long GetCustomerCount () {CustomerReadlock.lock (); Long Count = CustomerCount; CustomerReadlock.unlock (); recuento de retorno; } public Long GetShippingCount () {shippingReadlock.lock (); Long Count = ShippingCount; ShippingReadlock.unlock (); recuento de retorno; }}Todas las operaciones de lectura están protegidas por bloqueos de lectura, y todas las operaciones de escritura están protegidas por bloqueos de escritura. Si las operaciones de lectura realizadas en el programa son mucho más grandes que las operaciones de escritura, esta implementación puede traer mayores mejoras de rendimiento que la sección anterior porque las operaciones de lectura se pueden realizar simultáneamente.
1.3.3 Bloqueo de separación
El ejemplo anterior muestra cómo separar un solo bloqueo en múltiples bloqueos separados para que cada hilo pueda obtener el bloqueo del objeto que están a punto de modificar. Pero, por otro lado, este método también aumenta la complejidad del programa y puede causar muertos muertos si se implementa de manera inapropiada.
Un bloqueo de desprendimiento es un método similar a un bloqueo de desprendimiento, pero un bloqueo de desprendimiento es agregar un bloqueo para proteger diferentes fragmentos de código u objetos, mientras que un bloqueo de desprendimiento debe usar un bloqueo diferente para proteger diferentes rangos de valores. El paquete Java.util.concurrent utiliza esta idea para mejorar esta idea para mejorar el rendimiento de los programas que dependen en gran medida del hashmap. En términos de implementación, concurrenthashmap utiliza 16 cerraduras diferentes internamente, en lugar de encapsular un hashmap protegido sincrónicamente. Cada una de las 16 cerraduras es responsable de proteger el acceso sincrónico a una décima parte de los bits (cubos). De esta manera, cuando diferentes hilos desean insertar claves en diferentes segmentos, las operaciones correspondientes estarán protegidas por diferentes bloqueos. Pero también traerá algunos malos problemas, como la finalización de ciertas operaciones ahora requiere múltiples cerraduras en lugar de un bloqueo. Si desea copiar todo el mapa, se deben obtener los 16 bloqueos para completar.
1.3.4 Operación atómica
Otra forma de reducir la competencia de bloqueo es usar operaciones atómicas, lo que explicará los principios en otros artículos. El paquete java.util.concurrent proporciona clases encapsuladas atómicamente para algunos tipos de datos básicos de uso común. La implementación de la clase de operación atómica se basa en la función "Permutación de comparación" (CAS) proporcionada por el procesador. La operación CAS solo realizará una operación de actualización cuando el valor del registro actual es el mismo que el valor anterior proporcionado por la operación.
Este principio se puede utilizar para aumentar el valor de una variable de manera optimista. Si nuestro hilo conoce el valor actual, intentará usar la operación CAS para realizar la operación de incremento. Si otros hilos han modificado el valor de la variable durante este período, el llamado valor de corriente proporcionado por el hilo es diferente del valor real. En este momento, el JVM intenta recuperar el valor actual e intentarlo nuevamente, repitiéndolo nuevamente hasta que tenga éxito. Aunque las operaciones de bucle desperdiciarán algunos ciclos de CPU, el beneficio de hacerlo es que no necesitamos ninguna forma de control de sincronización.
La implementación de la clase de contador a continuación utiliza operaciones atómicas. Como puede ver, no se utiliza el código sincronizado.
Public static class contaratómica de implementos contador {private atomiclong customerCount = new AtomicLong (); shippingcount privado AtomicLong = new AtomicLong (); public void incrementCustomer () {CustomerCount.Incrementandget (); } public void incremementShipping () {ShippingCount.Incrementandget (); } public Long getCustomerCount () {return CustomerCount.get (); } public Long getShippingCount () {return shippingCount.get (); }}En comparación con la clase CounterSparatelock, el tiempo de ejecución promedio se ha reducido de 39 ms a 16 ms, que es de aproximadamente 58%.
1.3.5 Evite los segmentos de código de punto de acceso
Una implementación típica de la lista registra el número de elementos contenidos en la lista en sí al mantener una variable en el contenido. Cada vez que se elimina o se agrega un elemento de la lista, el valor de esta variable cambiará. Si la lista se usa en una aplicación única, este método es comprensible. Cada vez que llame al tamaño (), puede devolver el valor después del último cálculo. Si esta variable de recuento no se mantiene internamente por lista, cada llamada a tamaño () hará que la lista vuelva a traver y calcule el número de elementos.
Este método de optimización utilizado por muchas estructuras de datos se convertirá en un problema cuando se encuentre en un entorno multiproceso. Supongamos que compartimos una lista entre múltiples hilos, y múltiples hilos agregan o eliminamos elementos simultáneamente en la lista, y consultamos la gran longitud. En este momento, la lista de Count Variable Inside se convierte en un recurso compartido, por lo que todo el acceso a ella debe procesarse sincrónicamente. Por lo tanto, las variables de conteo se convierten en un punto caliente en toda la implementación de la lista.
El siguiente fragmento de código muestra este problema:
public static Class CarrePositoryWithCounter implementa CarRepository {private map <string, car> car = new Hashmap <String, Car> (); mapa privado <string, car> camiones = new Hashmap <String, Car> (); Objeto privado CharcountSync = new Object (); Private int Carcount = 0; public void addCar (coche de automóvil) {if (car.getliceClate (). startswith ("c")) {sincronizado (cars) {car foedCar = cars.get (car.getliceClate ()); if (FoundCar == NULL) {Cars.put (car.getliceClate (), car); sincronizado (carcountsync) {carcount ++; }}}} else {sincronizado (camiones) {Car FoundCar = Trucks.get (Car.GetLicEnlePlate ()); if (FoundCar == NULL) {Trucks.put (car.getlicEnsplate (), car); sincronizado (carcountsync) {carcount ++; }}}}}} public int getCarCount () {SynChronized (carcountSync) {return carcount; }}}La implementación anterior de la caricina tiene dos variables de lista en el interior, una se usa para colocar el elemento de lavado de autos y el otro se usa para colocar el elemento del camión. Al mismo tiempo, proporciona un método para consultar el tamaño total de estas dos listas. El método de optimización utilizado es que cada vez que se agrega un elemento de automóvil, se aumentará el valor de la variable de conteo interno. Al mismo tiempo, la operación incrementada está protegida por sincronizada, y lo mismo es cierto para devolver el valor de conteo.
Para evitar esta sobrecarga de sincronización de código adicional, consulte otra implementación de la caricina a continuación: ya no usa una variable de conteo interna, pero cuenta este valor en tiempo real en el método de devolver el número total de automóviles. como sigue:
public static Class CarroPositorywithoutCounter implementa CarRepository {private map <string, car> car = new Hashmap <String, Car> (); mapa privado <string, car> camiones = new Hashmap <String, Car> (); public void addCar (coche de automóvil) {if (car.getliceClate (). startswith ("c")) {sincronizado (cars) {car foedCar = cars.get (car.getliceClate ()); if (FoundCar == NULL) {Cars.put (car.getliceClate (), car); }}} else {sincronizado (camiones) {Car FoundCar = Trucks.get (Car.GetlicEnsplate ()); if (FoundCar == NULL) {Trucks.put (car.getlicEnsplate (), car); }}}}} public int getCarCount () {sincronizado (autos) {sincronizado (camiones) {return cars.size () + camionss.size (); }}}}Ahora, solo en el método getCarCount (), el acceso de las dos listas necesita protección de sincronización. Al igual que la implementación anterior, la sobrecarga de sincronización cada vez que ya no existe un nuevo elemento.
1.3.6 Evite la reutilización de caché de objetos
En la primera versión de Java VM, la sobrecarga del uso de la nueva palabra clave para crear nuevos objetos es relativamente alto, por lo que muchos desarrolladores están acostumbrados a usar el modo de reutilización de objetos. Para evitar la creación repetida de objetos una y otra vez, los desarrolladores mantienen un grupo de amortiguadores. Después de cada creación de instancias de objetos, se pueden guardar en el grupo de búfer. La próxima vez que otros hilos necesiten usarlos, se pueden recuperar directamente de la piscina del búfer.
A primera vista, este método es muy razonable, pero este patrón puede causar problemas en aplicaciones multiproceso. Debido a que el conjunto de objetos de búfer se comparte entre múltiples hilos, todas las operaciones de todos los hilos al acceder a objetos en ellos necesitan protección síncrona. La sobrecarga de esta sincronización es mayor que la creación del objeto en sí. Por supuesto, crear demasiados objetos aumentará la carga de la recolección de basura, pero incluso teniendo esto en cuenta, es mejor evitar las mejoras de rendimiento traídas al sincronizar el código que usar el grupo de caché de objetos.
Los esquemas de optimización descritos en este artículo una vez más muestran que cada método de optimización posible debe evaluarse cuidadosamente cuando realmente se aplica. Una solución de optimización inmadura parece tener sentido en la superficie, pero de hecho es probable que se convierta en un cuello de botella de rendimiento a su vez.