java - safe - ¿Se garantiza que la iteración a través de Collections.synchronizedSet(…).forEach() es segura para subprocesos?
thread pool java (2)
Como sabemos, la iteración sobre una colección concurrente no es segura para subprocesos de manera predeterminada, por lo que no se puede usar:
Set<E> set = Collections.synchronizedSet(new HashSet<>());
//fill with data
for (E e : set) {
process(e);
}
Esto sucede cuando se pueden agregar datos durante la iteración, porque no hay un bloqueo exclusivo en el set
.
Esto se describe en el javadoc de Collections.synchronizedSet
:
Conjunto estático público conjunto sincronizado (conjunto s)
Devuelve un conjunto sincronizado (seguro para subprocesos) respaldado por el conjunto especificado. Para garantizar el acceso en serie, es fundamental que todo el acceso al conjunto de respaldo se realice a través del conjunto devuelto.
Es imperativo que el usuario sincronice manualmente en el conjunto devuelto al iterar sobre él:
Set s = Collections.synchronizedSet (new HashSet ());
...
synchronized (s) { Iterator i = s.iterator(); // Must be in the synchronized block while (i.hasNext()) foo(i.next()); }
El incumplimiento de este consejo puede resultar en un comportamiento no determinista.
Sin embargo , esto no se aplica a Set.forEach
, que hereda el método predeterminado forEach
de Iterable.forEach .
Ahora miré el código fuente, y aquí podemos ver que tenemos la siguiente estructura:
- Pedimos un
Collections.synchronizedSet()
. Conseguimos uno:
public static <T> Set<T> synchronizedSet(Set<T> s) { return new SynchronizedSet<>(s); } ... static class SynchronizedSet<E> extends SynchronizedCollection<E> implements Set<E> { private static final long serialVersionUID = 487447009682186044L; SynchronizedSet(Set<E> s) { super(s); } SynchronizedSet(Set<E> s, Object mutex) { super(s, mutex); } public boolean equals(Object o) { if (this == o) return true; synchronized (mutex) {return c.equals(o);} } public int hashCode() { synchronized (mutex) {return c.hashCode();} } }
Extiende
SynchronizedCollection
, que tiene los siguientes métodos interesantes junto a los obvios:// Override default methods in Collection @Override public void forEach(Consumer<? super E> consumer) { synchronized (mutex) {c.forEach(consumer);} } @Override public boolean removeIf(Predicate<? super E> filter) { synchronized (mutex) {return c.removeIf(filter);} } @Override public Spliterator<E> spliterator() { return c.spliterator(); // Must be manually synched by user! } @Override public Stream<E> stream() { return c.stream(); // Must be manually synched by user! } @Override public Stream<E> parallelStream() { return c.parallelStream(); // Must be manually synched by user! }
El mutex
utilizado aquí es el mismo objeto en el que se bloquean todas las operaciones de Collections.synchronizedSet
.
Ahora podemos, a juzgar por la implementación decir que es seguro para subprocesos usar Collections.synchronizedSet(...).forEach(...)
, pero ¿también es seguro para subprocesos por especificación ?
(Lo suficientemente confuso, Collections.synchronizedSet(...).stream().forEach(...)
no es seguro para subprocesos por implementación, y el veredicto de la especificación parece ser también desconocido.)
Como escribió, a juzgar por la implementación, forEach()
es seguro para subprocesos para las colecciones provistas con JDK (vea la cláusula de exención de responsabilidad a continuación) ya que requiere que se adquiera un monitor de forEach()
para continuar.
¿Es también hilo seguro por especificación?
Mi opinión - no, y aquí hay una explicación. Collections.synchronizedXXX()
javadoc, reescrito en palabras cortas, dice: "todos los métodos son seguros para subprocesos, excepto los que se usan para iterar sobre él".
Mi otro argumento, aunque muy subjetivo, es lo que escribió yshavit , a menos que se lo diga / lea, considere API / clase / lo que no sea seguro para subprocesos.
Ahora, echemos un vistazo más de cerca a los javadocs. Supongo que puedo afirmar que el método forEach()
se utiliza para iterar sobre él, por lo que, siguiendo los consejos de javadoc, deberíamos considerar que no es seguro para subprocesos, aunque es opuesto a la realidad (implementación).
De todos modos, estoy de acuerdo con la afirmación de yshavit de que la documentación debe actualizarse ya que es muy probable que sea una documentación, no un defecto de implementación. Pero, nadie puede decirlo con seguridad, excepto para los desarrolladores de JDK, consulte las preocupaciones a continuación.
El último punto que me gustaría mencionar en esta discusión: podemos asumir que la colección personalizada se puede envolver con Collections.synchronizedXXX()
, y la implementación de forEach()
de esta colección puede ser ... puede ser cualquier cosa. La colección podría realizar el procesamiento asíncrono de elementos dentro del método forEach()
, generar un hilo para cada elemento ... está limitado solo por la imaginación del autor, y el forEach()
sincronizado (mutex) no puede garantizar la seguridad del hilo en tales casos . Ese problema en particular podría ser la razón para no declarar el método forEach()
como seguro para subprocesos.
Vale la pena echar un vistazo a la documentación de Collections.synchronizedCollection
lugar de Collections.synchronizedSet()
ya que esa documentación ya se ha limpiado:
Es imperativo que el usuario sincronice manualmente en la colección devuelta al atravesarla a través de
Spliterator
,Spliterator
oStream
: ...
Creo que esto deja bastante claro que existe una distinción entre la iteración a través de un objeto que no sea la propia Collection
sincronizada y el uso de su método forEach
. Pero incluso con la redacción anterior, puede llegar a la conclusión de que existe tal distinción:
Es imperativo que el usuario sincronice manualmente en el conjunto devuelto al iterar sobre él : ...
(énfasis por mi)
Compare con la Iterable.forEach :
Realiza la acción dada para cada elemento del
Iterable
hasta que todos los elementos hayan sido procesados o la acción arroje una excepción.
Si bien el desarrollador tiene claro que debe haber una iteración (interna) para lograrlo, esta iteración es un detalle de la implementación. De la especificación dada, es solo una acción (meta) para realizar una acción para cada elemento.
Al usar ese método, el usuario no está iterando sobre los elementos y, por lo tanto, no es responsable de la sincronización mencionada en la documentación de Collections.synchronized…
Sin embargo, eso es un poco sutil y es bueno que la documentación de synchronizedCollection
enumere explícitamente los casos de sincronización manual y creo que la documentación de los otros métodos también debería adaptarse.