My use case requires a directly allocated ByteBuffer
that is written to once and thereafter read by many concurrent threads. All reads are absolute and so I'm never concerned with the buffer's state (position, limit, mark).
This article on byte buffers by Keith Gregory warns that even absolute reads are not considered thread-safe:
ByteBuffer
thread safety is covered in theBuffer
JavaDoc; the short version is that buffers are not thread-safe. Clearly, you can't use relative positioning from multiple threads without a race condition, but even absolute positioning is not guaranteed (regardless of what you might think after looking at the implementation classes).
(emphasis mine)
Because of this warning, I'm preceding every read from the byte buffer with a call to duplicate
. This is easy enough, but the extra object allocation on every single read has me curious why it's actually necessary.
Despite Keith's wizardly disclaimer, I did look at OpenJDK's implementation of an absolute read from a direct byte buffer:
public byte get(int i) {
return ((unsafe.getByte(ix(checkIndex(i)))));
}
You can see that it simply delegates to Unsafe.getByte(long)
, which "fetches a value from a given memory address".
I understand that different implementations could exist, but what reasonably couldn't be thread-safe about this operation? Does the Buffer
contract simply decline to guarantee thread-safety for absolute reads in order to avoid the confusion of a partially thread-safe class? Or if the warning is justified for concurrent writes, what about my situation, in which the byte buffer is unmodified after creation? Also, would anything change when using a MappedByteBuffer
instead?
Related:
Why are absolute reads from a ByteBuffer not considered thread-safe?
Given the nature of the direct memory buffer (think mmap and other hardware backed files) that can be underneath the ByteBuffer
, any external updates are guaranteed to not be properly synchronized -- not good. In addition, the fact that slice()
and duplicate()
are designed to use the same buffer means that although your ByteBuffer
may be used in a thread-safe manner, there may be no guarantee that the "source" buffer is. These complexities may be the reason why blanket not-for-threads warnings are used.
Because of this warning, I'm preceding every read from the byte buffer with a call to duplicate.
As you mention, different JDKs and/or Java versions may have different implementations but I agree that I don't see any problem with you using the buffer unsynchronized as long as you are careful.
ByteBuffer
must not being modified by any external entitiy.ByteBuffer
and make any mutating method calls (i.e.e put(...)
etc.) before the threads are created or the field holding the buffer needs to be volatile
.get(...)
non-mutating methods are used by the threads. This means that (like you mention) you can't depend on the value of the offset
field. But also be careful of the array()
method that returns the actual array buffer and not a copy. Any mutating of the returned array will not be thread-safe.Keith's article hints at some of this:
That said, you still have the issue of creating buffers: you need to synchronize access to the slice() or duplicate() call. One way to do this is to create all of your buffers before spawning threads. However, that may be inconvenient, especially if your buffer is internal to another class. An alternative is to use ThreadLocal...
Like all threaded issues, the devil is in the details here and I admit that I don't have much experience with the Unsafe.getByte(...)
type methods directly aside from ByteBuffer.wrap(...)
.
Also, would anything change when using a MappedByteBuffer instead?
Use of MappedByteBuffer
is what makes the #1 above of paramount importance. Also, again, any calls to load()
must happen in a thread safe way (see #2).
If there is any question, one thing to consider is to use ByteBuffer.wrap(...)
and not use the loading mechanism and the direct memory stuff. Certainly you should run some performance tests to see what the impact is because you may find that making this appropriately synchronized (in a memory sense) may be more expensive than doing the load "by hand". Be careful about premature optimizations.
Best of luck.