Abstract: In our next puzzle, we up the ante a bit. We prevent GC during the test() method by storing a strong reference to all our Vectors. Cock and bull story, or are we struggling to identity our biases?
Welcome to the 281st edition of The Java(tm) Specialists' Newsletter. Yes, I'm still in Crete, where the sun is shining and the cicadas are finally starting to emerge from the ground. In a few days time we will have their deafening song make all human communication impossible. But it's the sound of summer, and that's a good time of year :-)
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
A special thank you to all my wonderful Java Specialist readers for sending me a lot of great explanations to yesterday's puzzle. Most answers were correct, although there was one subtlety that so far everyone has missed. Here is a follow-up puzzle, but be careful as our identity should not be in our biases, yet it is. Or is it the other way round? Have fun :-)
In our VectorBench2
, instead of only storing the
Vector
inside the
ThreadLocal
, we also store it inside a shared
set. Since Vector
does not implement
Comparable
, we cannot use TreeSet
nor ConcurrentSkipListSet
. Also, the
hashCode()
method of Vector
is
calculated on all the elements and similarly
equals()
compares the contents. Since a
Vector
may change, it is not a good idea to use
that as a key in any hash set. In our particular case, all the
Vectors
contain the same elements, thus only one
of them would remain in the Set
. Neither
HashSet
nor ConcurrentHashMap.newKeySet()
are appropriate
sets for our use case. The only set that would work is the
IdentityHashMap
converted with
Collections.newSetFromMap()
to a Set
.
The IdentityHashMap
is not thread-safe, but we can
solve that problem by wrapping it with
Collections.synchronizedMap()
. Since we now hold
a reference to the Vector
until we leave the
method, we can be sure that it cannot be somehow garbage
collected early. (My dad would have called all this a
cock and bull story, but I assure you that it does
affect the outcome.)
import java.util.*; import java.util.stream.*; public class VectorBench2 { public static void main(String... args) { for (int i = 0; i < 10; i++) { test(false); test(true); } } private static void test(boolean parallel) { Set<List<Integer>> vectors = Collections.newSetFromMap( Collections.synchronizedMap( // should not rely on a mutating hashCode() new IdentityHashMap<>() ) ); IntStream range = IntStream.range(1, 100_000_000); if (parallel) range = range.parallel(); long time = System.nanoTime(); try { ThreadLocal<List<Integer>> lists = ThreadLocal.withInitial(() -> { List<Integer> result = new Vector<>(); vectors.add(result); // avoid GC during run for (int i = 0; i < 1024; i++) result.add(i); return result; }); range.map(i -> lists.get().get(i & 1023)).sum(); } finally { time = System.nanoTime() - time; System.out.printf("%s %dms%n", parallel ? "parallel" : "sequential", (time / 1_000_000)); } } }
The results are now all quite similar:
openjdk version "1.8.0_252" OpenJDK Runtime Environment (build 1.8.0_252-b14) OpenJDK 64-Bit Server VM (build 25.252-b14, mixed mode) sequential 1624ms parallel 220ms sequential 1607ms parallel 214ms sequential 1641ms parallel 218ms sequential 1629ms parallel 222ms sequential 1591ms parallel 202ms sequential 1612ms parallel 219ms sequential 1590ms parallel 203ms sequential 1783ms parallel 233ms sequential 1880ms parallel 239ms sequential 1791ms parallel 290ms openjdk version "15-ea" 2020-09-15 OpenJDK Runtime Environment (build 15-ea+23-1098) OpenJDK 64-Bit Server VM (build 15-ea+23-1098, mixed mode, sharing) sequential 2125ms parallel 323ms sequential 1710ms parallel 262ms sequential 2111ms parallel 233ms sequential 1578ms parallel 252ms sequential 1591ms parallel 249ms sequential 1606ms parallel 235ms sequential 1567ms parallel 224ms sequential 1633ms parallel 224ms sequential 1579ms parallel 229ms sequential 1584ms parallel 225ms openjdk version "15-ea" 2020-09-15 OpenJDK Runtime Environment (build 15-ea+28-1400) OpenJDK 64-Bit Server VM (build 15-ea+28-1400, mixed mode, sharing) sequential 1572ms parallel 244ms sequential 1522ms parallel 222ms sequential 1538ms parallel 218ms sequential 1532ms parallel 216ms sequential 1499ms parallel 222ms sequential 1501ms parallel 227ms sequential 1520ms parallel 217ms sequential 1471ms parallel 215ms sequential 1545ms parallel 233ms sequential 1503ms parallel 220ms
What is going on? Please let me know your guesses by return email. Best explanation gets a mention in the next newsletter :-)
Kind regards from Crete
Heinz
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.