Thread-Safety of an Iterator Over a Stream of SecureRandom Values
This article reviews using an iterator for a stream from Java's SecureRandom, showing how a stream iterator from a thread-safe object is not always thread-safe.
Join the DZone community and get the full member experience.Join For Free
I recently experienced an interesting scenario involving Java streams. The scenario surprised me or at least wasn't what I was expecting. Maybe I was unwary in my implementation, but it still gave me another reason to consider things before using streams. Not that streams are bad, on the contrary, I think they are a great feature in the Java language and very cool to use, in the right place.
The need was the following: provide a mechanism to indefinitely generate unique IDs that are “secure,” in the sense that they are hard to predict. At any point in time, an ID needs to be generated and handed over to a client. To implement this, the Java platform’s SecureRandom felt like the best way to do it. It provides random values that strong from a security standpoint supports a very wide range of values, and is thread-safe.
The problem is that even if the range is vast (say you’re generating long values, so you have 264 values), this may still not be enough, and you want to ensure no duplicate values are even generated. One way to do this is to store values in some structure such as a concurrent set (you could generate one using a
ConcurrentHashMap). It turns out this was a bit more complicated to implement than I would have thought, the exact reasons being beyond the scope here, but basically, that structure had to be managed throughout a complex lifecycle (e.g. need to clean up unused values).
After checking the SecureRandom class again, a simpler way of implementing this could be to leverage its stream support. Specifically, the
longs() method inherited by its
Random superclass would return “an effectively unlimited stream of pseudorandom
long values”, in the form of a
LongStream. Like any other stream, intermediate operations could be added, such as
distinct(), which is what we want here. Since I want to be able to generate a value from this stream at any point, the
iterator() method, which is a terminal operation, would be added to build our stream pipeline.
That definitely looks neater. No need to manage a separate data structure. Furthermore, the docs of the
longs() method state that “a pseudorandom
long value is generated as if it’s the result of calling the method
nextLong()“. So it’s as if I’m just asking the SecureRandom to generate for me the next value each time I iterate over the stream.
Unfortunately, it turns out this one-line approach is too good to be a true solution to my problem. The code was no longer thread-safe. A simple multi-threaded program involving this stream would generate all kinds of race condition errors. The stream and its iterator derived from a thread-safe object are not thread-safe themselves, so you would need to synchronize access to the iterator. Adding a synchronized block was an acceptable solution for my case, but in others using a concurrent set could have been better.
The main takeaway for me was that streams are more difficult to use than one would think. Even if the solution is simple and declarative in nature, sometimes you want to control the “how” part instead of the “what." Stream pipelines can hide important aspects of your code, such as thread-safety, which can end up in throwing exceptions with a bunch of stream low-level code in their stack traces, which are difficult to understand. Often the hiding of implementation aspects is desirable, but I should at least think about what these aspects are because there is a lot happening under the hood. In other words, think twice before using Java streams.
Published at DZone with permission of Mahmoud Anouti, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.