«Los amigos de verdad no se dejan escribir concurrencia a bajo nivel». – @karianna. Aquí tienes tu oportunidad para participar en una revisión global de código y tratar de averiguar qué está ocurriendo en código sincronizado.
Éste artículo es una traducción al castellano de la entrada original publicada, en inglés, por Dr. Heinz Kabutz en su número 241 de The Javatm Specialists’ Newsletter. Puedes consultar el texto original en Javaspecialists’ Newsletter #241: Concurrency Puzzle – System.arraycopy()
Este artículo se publica traducido en adictos, con permiso del autor, por David Gómez García, (@dgomezg) consultor tecnológico en Autentia, colaborador de JavaSpecialists e instructor certificado para impartir los cursos de JavaSpecialists.
[/box]
Bienvenidos a la edición número 241 de The Javatm Specialists’ Newsletter, escrito desde Chorafakia, Creta. La tarde antes del inicio de nuestra conferencia JCrete, tropecé mientras corría, y me hice un esguince en el pie izquierdo. Entré en pánico. Setenta asistentes de 25 países distintos me esperaban. Elegidos cada uno de ellos a mano. Teníamos excursiones que hacer, playas lejanas, ruinas antiguas que explorar… Así que tomé el consejo del entrenador de baloncesto de mi hijo: spray de hielo para reducir el dolor tan rápidamente como fuese posible. Las instrucciones estaban en griego. ¿pero tan complicado iba a ser?. Apunta, pulveriza y ¡Listo!. Salvo que sí que debería haerlas leido. Me produje una quemadura de tal tipo que todavía dos meses después mi fisioterapeuta me desaprueba con la cabeza cuando me ve. La caída fue relativamente pequeña. La quemadura no. ¡Leed siempre las instrucciones!. El mismo consejo sirve para el código concurrente. Puedes achicharrarte gravemente sin darte cuenta. Tú código puede que funcione en tu equipo, pero fallar en el servidor
NOVEDAD Echa un vistazo a nuestro nuevo curso «Extreme Java«, que combina concurrencia, un poco de de rendimiento y Java 8: «Extreme Concurrency & performance course for Java 8», que también impartimos en castellano.
Un problema de concurrencia – System.arraycopy()
Hemos tenido hace poco un divertido debate en Twitter sobre la importancia de la revisión de pares en el código concurrente. Es muy fácil cometer errores, incluso para los programadores más experimentados. Escribir código concurrente se parece un poco a bucear en cuevas: sólo el aficionado piensa que tiene la capacidad suficiente para ir solo.
Mi buen amigo Jack Shirazi me ha enviado un pequeño código que habíamos preparado juntos. Siendo el Jack de JavaPerformanceTuning.com, obviamente él ya sabía que el cuello de botella estaba en la creación de objetos, no en la contención. También era consciente de que el paralelismo era incorrecto. No obstante, al idear su experimento, encontró un caso muy interesante de escritura temprana en System.arraycopy()
, debido a las líneas de caché, a hilos cambiando de núcleo, a la limpieza, etc…. Creo que le dejaré explicarse:
«Estaba jugando con locks separados para lecturas y escrituras, para un ejemplo en el que estoy pensando, y me encontré este fallo de concurrencia que no pude explicar. (Si no estás interesado no hay problema, es sólo una rareza fruto de una incorrecta implementación, así que no es algo tan útil). La clase está adjunta. Básicamente debería ser una clase thread-safe para una especie de arraylist para enteros con segmentación de monitores si uso un sólo lock, pero trataba de entender dónde se produciría el fallo de concurrencia si los hilos de lectura y los de escritura usaban distintos locks.
«Y lo que veo es que el hilo de lectura rara vez ve un elemento de array vacío, una vez que ha sido rellenado. No me sorprende que vea un estado corrupto, pero no puedo imaginarme cómo se puede llegar a ese estado corrupto. Quizá estoy poniendo demasiado esfuerzo simplemente en comprender una implementación incorrecta. Un par de cosas que he probado: como array de Objetos (más que array de int), obtengo el mismo fallo (ver un valor nulo); sin el método remove()
, de nuevo lo veo, solo que menos a menudo.
Casi pareciera que System.arraycopy()
establece un array vacío en el que copia los datos (posible), lo que estaría bien dentro de un bloque sincronizado, no verías ningún efecto, pero es como si ese array vacío se escapase a la memoria ‘principal’ antes de que el bloque sincronizado acabe, y el hilo de lectura lo ve.
«Mi mejor teoría es que el hilo de escritura está ocupado escribiendo y que escribe en la caché L1, vaciando primero la linea de cache, entonces, antes de escribir los datos, queda suspendido (hay muchas ejecuciones del GC); el hilo de lectura está también suspendido en una lectura; cuando los hilos son re-iniciados, el SO arranca el hilo de lectura en el núcleo en el que la escritura fue suspendida, y dado que no ha habido un vaciado del hilo de lectura que invalidase los contenidos de memoria de la cache, por eso el hilo de lectura asume que es válido y simplemente usa ese valor vacío.» – Jack Shirazi
Heinz de nuevo :-). Ejecuté el código en mi MacBookPro Retina Display y no me devolvió los resultados que él estaba obteniendo. Lo ejecuté después en mi servidor 2-4-1 (8 nucleos) en el que se hospeda JavaSpecialists.eu y allí pude ver que, de hecho, algunas veces get()
devolvía 0
cuando obviamente no debería. Así que es posible que necesites ejecutar el código en varias máquinas distintas hasta que puedas reproducirlo.
En lugar de simplemente proporcionar una explicación detallada, pensé que sería más divertido si os meto en la cueva de la programación concurrente con nosotros e intentamos averiguar cómo System.arraycopy()
puede, en ocasiones, realizar escrituras tempranas y dejar escapar arrays sin inicializar accesibles para los hilos de lectura. ¿quizá mirando al código C de System.arraycopy()
o utilizando JITWatch para ver el código máquina generado?. La primera persona que me envíe una explicación correcta, se ganará una mención en el próximo artículo. Si estás suscrito al newsletter por email, simplemente responde al correo. En otro caso, por favor, registrate aquí y responde al correo de bienvenida. Date prisa, hay cientos de desarrolladores Java leyendo esto como tú, deseando ser inmortalizados en el The Javatm Specialists’ Newsletter :-). Un segundo reto para aquellos de vosotros que estáis despiertos a estas horas es enviarme una solución que use StampedLock
. Para ganar puntos extra, actualiza el lock en el método remove()
import java.util.*; public class MyArrayList { private final Object READ_LOCK = new Object(); private final Object WRITE_LOCK = new Object(); private int[] arr = new int[10]; private int size = 0; public int size() { synchronized (READ_LOCK) { return size; } } public int get(int index) { synchronized (READ_LOCK) { rangeCheck(index); return arr[index]; } } public boolean add(int e) { synchronized (WRITE_LOCK) { if (size + 1 > arr.length) arr = Arrays.copyOf(arr, size + 10); arr[size++] = e; return true; } } public int remove(int index) { synchronized (WRITE_LOCK) { rangeCheck(index); int oldValue = arr[index]; int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(arr, index + 1, arr, index, numMoved); arr[--size] = 0; return oldValue; } } private void rangeCheck(int index) { if (index >= size) throw new IndexOutOfBoundsException( "Index: " + index + ", Size: " + size); } public static void main(String[] args) { for (int i = 0; i < 100000; i++) { MyArrayList list = new MyArrayList(); new Thread(new Main(list, true)).start(); new Thread(new Main(list, false)).start(); new Thread(new Main(list, false)).start(); } } static class Main implements Runnable { MyArrayList list; boolean update; public Main(MyArrayList list, boolean update) { this.list = list; this.update = update; } @Override public void run() { if (update) { for (int i = 1; i < 1000; i++) { list.add(i); } for (int i = 1; i < 250; i++) { list.remove(7); } } else { // wait until we're certain // index 6 has a value while (list.size() < 7) {} for (int i = 1; i < 1000; i++) { int x; if ((x = list.get(6)) != 7) { System.out.println(x + " and " + list.size()); } } } } } }
Disclaimer: Jack Shirazi me ha permitido amablemente republicar este correo y el código. La clase que me ha enviado es un experimento y no es indicativo de lo que escribe habitualmente :-). Sé exactamente que es lo que está ocurriendo y por qué, dado que se explica en el capítulo 2 de mi curso «Extreme Concurrency & performance course for Java 8», que también impartimos en castellano. Estoy convencido de que muchos de vosotros lo averiguareis también pero, por favor, tened espíritu deportivo y no publiquéis vuestras ideas hasta que hayamos anunciado el ganador. Paciencia, paciencia, paciencia 🙂
Saludos
Heinz.
P.D. Si todavía no te has apuntado a la newsletter mensual de Jack Shirazi, te recomiendo fuertemente que lo hagas. Aquí tienes el enlace: Java Performance Tuning News
[…] gracias a todos los que ya me han enviado por correo su solución al problema de la semana pasada. La mayoría de las respuestas eran correctas en teoría, pero incorrectas en la práctica . Por […]
[…] hemos divertido este último mes. Propuse un reto de concurrencia que ha tenido a expertos en Java de todo el mundo rascándose la cabeza. Tanto es así, que […]