Java
DecimalFormat
multithreading
thread-safety
concurrency

DecimalFormat.formatdouble in different threads

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

DecimalFormat is convenient for rendering numbers, but a shared instance is not safe to use from multiple threads. If several requests call format on the same formatter concurrently, you can get corrupted strings or intermittent failures that are very hard to reproduce.

Why the Shared Formatter Pattern Breaks

DecimalFormat keeps mutable internal state while converting numbers to text. That internal reuse is efficient for one thread, but dangerous when multiple threads interleave operations on the same instance.

This pattern looks clean but is unsafe:

java
1import java.text.DecimalFormat;
2
3public class UnsafeFormatter {
4    private static final DecimalFormat DF = new DecimalFormat("#,##0.00");
5
6    public static String format(double value) {
7        return DF.format(value);
8    }
9}

The code may pass unit tests and still fail under production load because concurrency issues often need sustained traffic before they show up.

Safe Option 1: Create a Formatter Per Call

The most straightforward fix is to create a new formatter each time you need one. That gives every call its own state and avoids cross-thread interference completely.

java
1import java.text.DecimalFormat;
2
3public class LocalFormatter {
4    public static String format(double value) {
5        DecimalFormat df = new DecimalFormat("#,##0.00");
6        return df.format(value);
7    }
8
9    public static void main(String[] args) {
10        System.out.println(format(12345.678));
11    }
12}

For many services, this is the right answer. The small allocation cost is usually less important than correctness.

Safe Option 2: Use ThreadLocal

If number formatting sits in a very hot path and you want to avoid creating a new object on every call, ThreadLocal provides one formatter per thread.

java
1import java.text.DecimalFormat;
2
3public class ThreadLocalFormatter {
4    private static final ThreadLocal<DecimalFormat> DF =
5        ThreadLocal.withInitial(() -> new DecimalFormat("#,##0.00"));
6
7    public static String format(double value) {
8        return DF.get().format(value);
9    }
10}

This can be a good compromise, but only if you understand the lifetime of the threads involved. Long-lived thread pools keep their thread-local state around.

Locale-Aware Formatting

Sometimes the real requirement is locale-aware output rather than one fixed numeric pattern. In that case, use a locale-specific formatter and keep locale selection explicit.

java
1import java.text.NumberFormat;
2import java.util.Locale;
3
4public class LocaleFormatter {
5    public static String format(double value, Locale locale) {
6        NumberFormat format = NumberFormat.getNumberInstance(locale);
7        format.setMinimumFractionDigits(2);
8        format.setMaximumFractionDigits(2);
9        return format.format(value);
10    }
11
12    public static void main(String[] args) {
13        System.out.println(format(12345.678, Locale.US));
14        System.out.println(format(12345.678, Locale.GERMANY));
15    }
16}

Even then, avoid sharing one mutable formatter globally across all threads and users.

A Small Stress Test

Concurrency problems often hide until many calls hit the formatter at once. A simple stress test helps surface unsafe sharing:

java
1import java.text.DecimalFormat;
2import java.util.ArrayList;
3import java.util.List;
4import java.util.concurrent.ExecutorService;
5import java.util.concurrent.Executors;
6import java.util.concurrent.Future;
7
8public class RaceDemo {
9    private static final DecimalFormat SHARED = new DecimalFormat("0.0000");
10
11    public static void main(String[] args) throws Exception {
12        ExecutorService pool = Executors.newFixedThreadPool(8);
13        List<Future<String>> futures = new ArrayList<>();
14
15        for (int i = 0; i < 2000; i++) {
16            final double value = i / 10.0;
17            futures.add(pool.submit(() -> SHARED.format(value)));
18        }
19
20        for (Future<String> future : futures) {
21            System.out.println(future.get());
22        }
23
24        pool.shutdown();
25    }
26}

This may not fail every run, which is exactly why the bug is dangerous. Shared mutable formatters can be "fine" until timing changes.

Choosing Between the Safe Options

Use local construction unless profiling proves it is too expensive. Reach for ThreadLocal only after you have a real throughput reason and a clear thread lifecycle. Avoid synchronized as a reflex fix unless you are comfortable serializing every formatting call through one lock.

Also remember that DateTimeFormatter from java.time is immutable and thread-safe, but that guarantee does not extend to DecimalFormat. Developers often assume all formatter types in Java behave the same way, and they do not.

Common Pitfalls

The most common mistake is declaring a shared formatter as static final and assuming final makes it thread-safe. It only makes the reference immutable, not the formatter's internal state.

Another issue is relying on light local testing. Concurrency bugs may not appear until executor pools, request bursts, or production-like traffic levels are involved.

Developers also forget that locale requirements can make a single global formatter logically wrong even before thread safety becomes a problem.

Finally, ThreadLocal is not free. It solves one problem, but it also creates lifecycle and cleanup considerations in managed runtimes.

Summary

  • 'DecimalFormat instances are mutable and not thread-safe.'
  • Sharing one formatter across threads can produce incorrect or unstable output.
  • The simplest safe fix is to create a new formatter per call.
  • 'ThreadLocal can reduce allocation in hot paths, but it adds lifecycle complexity.'
  • Keep locale handling explicit and avoid global mutable formatters by default.

Course illustration
Course illustration

All Rights Reserved.