Cyclic Pool for Shared Objects
I created this class in response to a question on StackOverflow.com. The question was regarding the most efficient way to use Java DateFormat
objects when you have many short-lived threads and no per-logical-thread context which survives the life of a thread.
My thought was to create a class which would dispense objects from a fix-sized pool of pre-created date formatters in a round-robin fashion. Given that uncontested synchronization is cheap nowadays, the individual formatters could be synchronized on when used, which would amortize synchronization collisions across the number of formatters in the pool. So there might be 50 formatters, each used in turn - collision, and therefore lock contention, would occur only if 51 dates were actually formatted simultaneously. The objects remain in the pool and are reused on demand.
I implemented a generic fixed pool as suggested above, with a test which exercised SimpleDateFormat when:
- Sharing one global formatter and synchronizing,
- Creating a new formatter every time (no synchronization necessary),
- Cloning a pre-created template formatter (no synchronization necessary), and
- Using a fixed pool of pre-created formatters, synchronizing.
Following are my results with SimpleDateFormat on a Quad Core AMD Phenom II 965 BE, running in the Java 6 SE client JVM (lower is better). The test was deliberately designed for extreme contention. Notably, cloning and pooling were very close together. In repeated runs, cloning was faster than pooling about as often as it was slower.
2011-02-19 15:28:13.039 : Threads=10, Iterations=1,000,000
2011-02-19 15:28:13.039 : Test 1:
2011-02-19 15:28:25.450 : Sync : 12,411 ms
2011-02-19 15:28:37.380 : Create : 10,862 ms
2011-02-19 15:28:42.673 : Clone : 4,221 ms
2011-02-19 15:28:47.842 : Pool : 4,097 ms
2011-02-19 15:28:48.915 : Test 2:
2011-02-19 15:29:00.099 : Sync : 11,184 ms
2011-02-19 15:29:11.685 : Create : 10,536 ms
2011-02-19 15:29:16.930 : Clone : 4,184 ms
2011-02-19 15:29:21.970 : Pool : 3,969 ms
2011-02-19 15:29:23.038 : Test 3:
2011-02-19 15:29:33.915 : Sync : 10,877 ms
2011-02-19 15:29:45.180 : Create : 10,195 ms
2011-02-19 15:29:50.320 : Clone : 4,067 ms
2011-02-19 15:29:55.403 : Pool : 4,013 ms
A little surprising is that cloning is less good when single threaded. Also interesting is that the the pool performs on par with a single synchronized object. This would imply that the pool is the best alternative overall, since it delivers excellent performance when contended and when uncontended.
2011-02-20 13:26:58.169 : Threads=1, Iterations=10,000,000
2011-02-20 13:26:58.169 : Test 1:
2011-02-20 13:27:07.193 : Sync : 9,024 ms
2011-02-20 13:27:40.320 : Create : 32,060 ms
2011-02-20 13:27:53.777 : Clone : 12,388 ms
2011-02-20 13:28:02.286 : Pool : 7,440 ms
2011-02-20 13:28:03.354 : Test 2:
2011-02-20 13:28:10.777 : Sync : 7,423 ms
2011-02-20 13:28:43.774 : Create : 31,931 ms
2011-02-20 13:28:57.244 : Clone : 12,400 ms
2011-02-20 13:29:05.734 : Pool : 7,417 ms
2011-02-20 13:29:06.802 : Test 3:
2011-02-20 13:29:14.233 : Sync : 7,431 ms
2011-02-20 13:29:47.117 : Create : 31,816 ms
2011-02-20 13:30:00.567 : Clone : 12,382 ms
2011-02-20 13:30:09.079 : Pool : 7,444 ms
Even more surprising was a colleague’s test on his 1.66 GHz Core2 laptop where interestingly the create was massively more expensive under overload, probably because GC hits harder, and there is more task switching:
2011-02-20 10:48:24.281 : Threads=10, Iterations=1,000,000
2011-02-20 10:48:24.281 : Test 1:
2011-02-20 10:48:54.500 : Sync : 30,219 ms
2011-02-20 10:51:12.156 : Create : 136,610 ms
2011-02-20 10:52:09.203 : Clone : 55,985 ms
2011-02-20 10:52:26.140 : Pool : 15,875 ms
2011-02-20 10:52:27.203 : Test 2:
2011-02-20 10:52:55.125 : Sync : 27,922 ms
2011-02-20 10:55:05.765 : Create : 129,578 ms
2011-02-20 10:56:02.546 : Clone : 55,718 ms
2011-02-20 10:56:19.734 : Pool : 16,125 ms
2011-02-20 10:56:20.796 : Test 3:
2011-02-20 10:56:49.593 : Sync : 28,797 ms
2011-02-20 10:59:00.671 : Create : 130,015 ms
2011-02-20 10:59:57.765 : Clone : 56,031 ms
2011-02-20 11:00:17.468 : Pool : 18,640 ms
In the specific case of the SimpleDateFormat, I think I might be tempted to just create a template and clone it on demand. In the more general case, I might be tempted to use this pool for such things.
Before making a final decision one way or the other, I would want to thoroughly test on a variety of JVMs, versions and for a variety of these kinds of objects. Older JVMs, and those on small devices like handhelds and phones might have much more overhead in object creation and garbage collection. Conversely, they might have more overhead in uncontested synchronization.
FWIW, from my review of the code, it seemed that SimpleDateFormat would most likely be representative of a relatively large amount of work to do in being cloned.
Get The Source
The source compiles to Java 7, but only minor changes should be required to target Java 6 or earlier.
Download CyclicSharePool.java.
Download CyclicSharePool_Test.java