Iocpclass that performs all the asynchronous I/O tasks in NIO.2 file channels is, by default, backed by a so called "cached" thread pool. That's a thread pool that creates new threads as needed, but will reuse previously constructed threads *when* they are available. Look at the code of the
ThreadPoolclass held by the
The thread pool in the default channel group is constructed as
ThreadPoolExecutorwith a maximum thread count of Integer.MAX_VALUE and a keep-alive-time of Long.MAX_VALUE. The threads are created as daemon threads by the thread factory. A synchronous hand-over queue is used to trigger thread creation if all threads are busy. There are several issues with this configuration:
1. If you perform write operations on asynchronous channels in a burst you will create thousands of worker threads which likely results in an
OutOfMemoryError: unable to create new native thread.
2. When the JVM exits, then all deamon threads are abandoned - finally blocks are not executed, stacks are not unwound.
In my other blog I have explained why unbounded thread pools can 'cause trouble. Therefore, if you use asynchronous file channels, it may be an option to use custom thread pools instead of the default thread pool. The following snippet shows an example custom setting.
The javadoc of
AsynchronousFileChannelstates that the custom executor should "minimally [...] support an unbounded work queue and should not run tasks on the caller thread of the execute method." That's a risky statement, it is only reasonable if resources aren't an issue, which is rarely the case. It may make sense to use bounded thread pools for asynchronous file channels. You cannot get a too-many-threads issue, also you cannot flood your heap with work queue tasks. In the example above you have five threads that execute asynchonous I/O tasks and the work queue has a capacity of 2500 tasks. If the capacity limit is exceeded the rejected-execution-handler implements the
CallerRunsPolicywhere the client has to execute the write task synchronously. This can (dramatically) slow down the system performance because the workload is "pushed back" to the client and executed synchronously. However, it can also save you from much more severe issues where the result is unpredictable. It's a good practice to work with bounded thread pools and to keep the thread pool sizes configurable, so that you can adjust them at runtime. Again, to learn more about robust thread pool settings see my other blog entry.
Thread pools with synchronous hand-over queues and unbound maximum thread pool sizes can aggressively create new threads and thus can seriously harm system stability by consuming (pc registers and java stacks) runtime memory of the JVM. The 'longer' (elapsed time) the asynchronous task, the more likely you'll run into this issue.
Thread pools with unbounded work queues and fixed thread pool sizes can aggressively create new tasks and objects and thus can seriously harm system stability by consuming heap memory and CPU through excessive garbage collection activity. The larger (in size) and longer (in elapsed time) the asynchronous task the more likely you'll run into this issue.
That's all in terms of applying custom thread pools to asynchronous file channels. My next blog in this series will explain how to close asynchronous channels safely without loosing data.
The NIO.2 file channels series:
- Applying custom thread pools
- Closing file channels without loosing data
- I/O operations are not atomic