Thursday, April 5, 2012

Java 7: NIO.2 File Channels on the test bench - Part 2 - Applying custom thread pools

Asynchronous file processing isn't a green card for high performance. In my last post I have demonstrated that conventional I/O can be faster then asynchronous channels. There are some additional important facts to know when applying NIO.2 file channels. The Iocp class 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 ThreadPool class held by the Iocp.


The thread pool in the default channel group is constructed as ThreadPoolExecutor with 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 AsynchronousFileChannel states 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 CallerRunsPolicy where 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:
- Introduction
- Applying custom thread pools
- Closing file channels without loosing data
- I/O operations are not atomic




5 comments:

  1. Hi Niklas
    Your blog is too good. Could you please post a blog on how to gain knowledge on architectural side of things. What i mean is could you please write on how to move to the next level from programming to design and architecture. Any Thanks in advance.

    ReplyDelete
  2. Hi there! I was thinking about it for a while, an architecture series. It's interesting that you call it the 'next level'. I started as a developer and then entered 'the next level' and worked as an architect. I didn't do much programming for years. And then I've realized that my judgements/evaluations got worse. Since that time I'm spending 50% of my time with programming, since there is no 'technical' architect w/o superior up-to-date development (API, framework etc.) expertise. Therefore I wouldn't call it 'the next level' anymore, it's about sharing knowledge, community and taking responsibility in a team of technical experts. May be what I just said is a good start for an architecture series. Thx for the very nice comment!! Cheers, Niklas

    ReplyDelete
  3. Hmm to the point. Hope to see your architecture series soon :)

    ReplyDelete
  4. Hi Niklas. Thanks for the articles....

    What is the effect of the calling thread issuing yield on itself when the CallerRunsPolicy is active?

    ReplyDelete
    Replies
    1. I would think that the client issues asynchronous I/O operations to the underlying operating system and all pool threads handle the I/O events in the I/O completion port. There is no queueing of WriteTasks to the work queue in use.

      Delete