import java.util.Date; import java.util.List; import java.util.ArrayList; import java.text.SimpleDateFormat; /** * Attempt various strategies for making SimpleDateFormat safe, and report the * runtime of each approach. * * @author Jesse Wilson */ public class SDFPerformance { /** the pattern used when encoding dates */ private static final String SIMPLE_DATE_FORMAT_PATTERN = "EEE, d MMM yyyy HH:mm:ss Z"; /** * Describe a strategy to make SimpleDateFormat safe. */ public interface DateFormatAccess { public String format(Date date); } /** * Create a new SimpleDateFormat on every invocation. */ public static class SharedDateFormatAccess implements DateFormatAccess { private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN); public String format(Date date) { return simpleDateFormat.format(date); } } /** * Create a new SimpleDateFormat on every invocation. */ public static class NewInstanceDateFormatAccess implements DateFormatAccess { public String format(Date date) { return new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN).format(date); } } /** * Use the synchronized() keyword. */ public static class SynchronizedInstanceDateFormatAccess implements DateFormatAccess { private SimpleDateFormat dateFormat = new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN); public synchronized String format(Date date) { return dateFormat.format(date); } } /** * Use a pool of SimpleDateFormat objects, implemented using an ArrayList. */ public static class ListPoolDateFormatAccess implements DateFormatAccess { private List pool = new ArrayList(10); public String format(Date date) { SimpleDateFormat dateFormat; synchronized(pool) { int size = pool.size(); if(size == 0) dateFormat = new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN); else dateFormat = (SimpleDateFormat)pool.remove(size - 1); } String result = dateFormat.format(date); synchronized(pool) { pool.add(dateFormat); } return result; } } /** * Use ThreadLocals for the implementation. */ public static class ThreadLocalDateFormatAccess implements DateFormatAccess { ThreadLocal threadLocal = new DateFormatThreadLocal(); public String format(Date date) { return threadLocal.get().format(date); } private static class DateFormatThreadLocal extends ThreadLocal { protected SimpleDateFormat initialValue() { return new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN); } } } /** * Run a sequence of warm ups followed by a sequence of tests. */ public static void main(String[] args) { System.out.println("WARM UP: 100 threads x 1000 executions"); System.out.println(test(new SharedDateFormatAccess(), 1000, 100)); System.out.println(test(new NewInstanceDateFormatAccess(), 1000, 100)); System.out.println(test(new SynchronizedInstanceDateFormatAccess(), 1000, 100)); System.out.println(test(new ListPoolDateFormatAccess(), 1000, 100)); System.out.println(test(new ThreadLocalDateFormatAccess(), 1000, 100)); System.out.println("RUN: 100 threads x 1000 executions"); System.out.println(test(new SharedDateFormatAccess(), 1000, 100)); System.out.println(test(new NewInstanceDateFormatAccess(), 1000, 100)); System.out.println(test(new SynchronizedInstanceDateFormatAccess(), 1000, 100)); System.out.println(test(new ListPoolDateFormatAccess(), 1000, 100)); System.out.println(test(new ThreadLocalDateFormatAccess(), 1000, 100)); } /** * Test a particular implemenation and return the result. */ private static TestResult test(final DateFormatAccess dateFormatAccess, final int roundsPerThread, final int threadCount) { final TestResult testResult = new TestResult(); testResult.input = dateFormatAccess.getClass(); testResult.roundsPerThread = roundsPerThread; testResult.threadCount = threadCount; final Date dateA = new Date(System.currentTimeMillis()); final Date dateB = new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 7)); final Date dateC = new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24 * 31)); final String expectedA = new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN).format(dateA); final String expectedB = new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN).format(dateB); final String expectedC = new SimpleDateFormat(SIMPLE_DATE_FORMAT_PATTERN).format(dateC); final Thread[] threads = new Thread[threadCount]; for(int i = 0; i < threadCount; i++) { final int previous = i - 1; threads[i] = new Thread(new Runnable() { public void run() { try { for(int j = 0; j < roundsPerThread; j++) { String resultA = dateFormatAccess.format(dateA); if(!expectedA.equals(resultA)) throw new IllegalStateException("Expected " + expectedA + " but found " + resultA); String resultB = dateFormatAccess.format(dateB); if(!expectedA.equals(resultA)) throw new IllegalStateException("Expected " + expectedB + " but found " + resultB); String resultC = dateFormatAccess.format(dateC); if(!expectedA.equals(resultA)) throw new IllegalStateException("Expected " + expectedC + " but found " + resultC); Thread.sleep(1); } if(previous > 0) threads[previous].join(); } catch (RuntimeException e) { testResult.exceptions.add(e); } catch (InterruptedException e) { System.exit(1); } } }); } long start = System.currentTimeMillis(); for(int i = 0; i < threadCount; i++) { threads[i].start(); } try { threads[threadCount - 1].join(); } catch (InterruptedException e) { e.printStackTrace(); } long end = System.currentTimeMillis(); testResult.runtime = end - start; return testResult; } /** * The consequence of a test execution. */ private static class TestResult { int threadCount; int roundsPerThread; Class input; long runtime; final List exceptions = new ArrayList(); public String toString() { if(exceptions.isEmpty()) { return "threads: " + threadCount + ", rounds: " + roundsPerThread + ", implementation " + input.getName() + ": time: " + runtime + "ms"; } else { return "threads: " + threadCount + ", rounds: " + roundsPerThread + ", implementation " + input.getName() + " failed"; } } } }