Table of Contents |
---|
Overview
PlantUML |
---|
@startuml interface AutoCloseable { void close() } interface ByteBufferSharing extends AutoCloseable { ByteBuffer getBuffer() ByteBuffer expandWriteBufferIfNeeded(int newCapacity) ByteBuffer expandReadBufferIfNeeded(int newCapacity) } class ByteBufferVendor { ByteBufferVendor(ByteBuffer bufferArg, BufferType bufferType, BufferPool bufferPool) ByteBufferSharing open() ByteBufferSharing open(long time, TimeUnit unit) void destruct() } interface ByteBufferVendor.ByteBufferSharingInternal extends ByteBufferSharing { void releaseBuffer() } class ByteBufferVendor.ByteBufferSharingInternalImpl implements ByteBufferVendor.ByteBufferSharingInternal {} ByteBufferVendor "sharing" *-- ByteBufferVendor.ByteBufferSharingInternal ByteBufferVendor "lock" *-- ReentrantLock ByteBufferVendor "isDestructed" *-- AtomicBoolean ByteBufferVendor "counter" *-- AtomicInteger ByteBufferVendor.ByteBufferSharingInternalImpl "buffer\n0..1" --> "1" ByteBuffer ByteBufferVendor.ByteBufferSharingInternalImpl "bufferPool\n*" --> "1" BufferPool ByteBufferVendor.ByteBufferSharingInternalImpl "bufferType\n*" --> "1" BufferType ByteBufferVendor .. ByteBuffer : derived\nassociation @enduml |
...
In the class diagram at the top of this page you'll see a dotted association between ByteBufferVendor and ByteBuffer labeled "derived association". That's meant to convey the fact that the whole point of the vendor is that it mediates access to a ByteBuffer. In UML terms, there is a derived association between the two, even though you won't see an explicit (direct) object reference in the code.
Following the Rules
A "resource" object returned from a ByteBufferSharing open() must be assigned in a try-with-resources variable declaration.
...
It's ok to pass a resource reference to a method so long as you are sure that method (and methods it calls) do not allow the reference value to escape. But it is, in general, not ok to return a resource reference from a method (caveats in the the Writing Your Own Resource-Returning Methods section below). An escape happens when the reference value:
- is stored anywhere other than the stack
- is communicated to another thread
- is returned from a method without the precautions described in the Writing Your Own Resource-Returning Methods section below
If you are careful to avoid the first two issues above, then it's ok to pass a resource reference to another method. The resource is available for the lifetime of the method call. And you can be sure that the reference value has not leaked.
Ownership and Reference Counting
A ByteBufferSharing object is typically a field of some object that "owns" the underlying (logical) ByteBuffer. When the owning object is done with that (logical) ByteBuffer forever, it calls destruct() on the ByteBufferSharing. This is necessary because the ByteBuffer must be explicitly returned to a pool.
...
Thread C: try-with-resources block ends and reference count is 0, NOW the ByteBuffer can be returned to its pool
Writing Your Own Resource-Returning Methods
The ByteBufferSharing open() methods are special. They return a ByteBufferSharing object that constitutes a capability object—if you have the object, you can always perform the operation (in this case the operation is the various methods on the ByteBufferSharing interface).
In practice, you may want to write your own method that returns a ByteBufferSharing. That's fine so long as callers of your new method follow the same rules they'd follow for open() (see the the Following the Rules section above).
But you have to be careful when implementing your method. You'd like to ensure that callers of your method don't risk losing the resource between your method returning, and the resource reference value being assigned in the caller's try-with-resources.
...
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
try (final ByteBufferSharing sharing = foo()) { // access ByteBuffer through sharing reference here } |
Implementer's Notes
When you first encounter ByteBufferVendor.ByteBufferSharingInternalImpl you may wonder why we chose to have both a counter and an isDestructed flag. Couldn't we have just allowed a count of zero to trigger destruction?
The reason we needed both is apparent if you look at the second example in the the Ownership and Reference Counting section. We designed the framework so that the ultimate owner of the resource, the object that has a field of type ByteBufferSharing, can "go away" (and call destruct()) while the byte buffer is still in use by other threads. When that happens we set the isDestructed flag and disallow further open() calls. But, critically, we allow an in-flight try-with-resources block to continue its work. We avoid returning the buffer to the pool until the resource owner has destruct()ed the resource and all try-with-resources blocks have finished.