SimpleDateFormat thread safety problem repair solution | JD Logistics technical team

problem introduction

In the daily development process, we will inevitably use the Date class before JDK8, and we need to use the SimpleDateFormat class when formatting dates or parsing dates, but because this class is not thread-safe, we often find that the Improper use of this class will cause date parsing exceptions, thereby affecting the availability of online services.

The following is sample code for inappropriate use of the SimpleDateFormat class:

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/25 10:47
 * @Desc: SimpleDateFormat 线程安全问题复现
 * @Version: V1.0
 **/
public class SimpleDateFormatTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();
    private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-M-d"); // 非线程安全

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    FORMATTER.parse("2023-7-15");
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }
}


The above code simulates the scenario where the SimpleDateFormat instance is used concurrently by multiple threads. At this time, the following abnormal output can be observed:

java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2082)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2082)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)
java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2087)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)
java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2087)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2082)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at com.jd.threadsafe.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:21)
	at java.lang.Thread.run(Thread.java:750)


The root cause of the above exception is that SimpleDateFormat is stateful . For example, the SimpleDateFormat class contains a non-thread-safe NumberFormat member variable:

/**
 * The number formatter that <code>DateFormat</code> uses to format numbers
 * in dates and times.  Subclasses should initialize this to a number format
 * appropriate for the locale associated with this <code>DateFormat</code>.
 * @serial
 */
protected NumberFormat numberFormat;


From the Java Doc of NumberFormat, you can see the following description:

Synchronization Number formats are generally not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

From the Java Doc of SimpleDateFormat, you can see the following description:

Synchronization Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

Solution 1: Locking (not recommended)

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/25 10:47
 * @Desc: SimpleDateFormat 线程安全修复方案:加锁
 * @Version: V1.0
 **/
public class SimpleDateFormatLockTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();
    private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("yyyy-M-d"); // 非线程安全

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    synchronized (FORMATTER) {
                        FORMATTER.parse("2023-7-15");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}


First of all, the simplest repair solution we can think of to solve the thread safety problem is locking. For the above repair plan, use the synchronized keyword to lock the FORMATTER instance. At this time, when multi-threading performs date formatting, it degenerates into serial execution. Guarantees correctness but sacrifices performance , not recommended.

Solution 2: Stack closure (not recommended)

If you follow the recommended usage in the document, you can see that it is recommended to create an independent SimpleDateFormat instance for each thread. One of the easiest ways is to create a SimpleDateFormat instance each time the method is called to achieve the effect of stack closure, as shown in the following sample code :

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/25 10:47
 * @Desc: SimpleDateFormat 线程安全修复方案:栈封闭
 * @Version: V1.0
 **/
public class SimpleDateFormatStackConfinementTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    new SimpleDateFormat("yyyy-M-d").parse("2023-7-15");
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}


The shared SimpleDateFormat instance is adjusted to create a new instance each time. This fix ensures correctness, but each method call needs to create a SimpleDateFormat instance, and the SimpleDateFormat instance is not reused. There is GC loss , so it is not recommended.

Solution 3: ThreadLocal (recommended)

If the date formatting operation is a high-frequency operation in the application, and performance needs to be guaranteed first, then it is recommended that each thread reuse the SimpleDateFormat instance. At this time, the ThreadLocal class can be introduced to solve this problem:

package com.jd.threadsafe;

import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/25 10:47
 * @Desc: SimpleDateFormat 线程安全修复方案:ThreadLocal
 * @Version: V1.0
 **/
public class SimpleDateFormatThreadLocalTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();
    private static final ThreadLocal<SimpleDateFormat> SIMPLE_DATE_FORMAT_THREAD_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-M-d"));

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    SIMPLE_DATE_FORMAT_THREAD_LOCAL.get().parse("2023-7-15");
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}


Execute the above code, and no abnormal output will be observed anymore, because an independent SimpleDateFormat instance has been created for each thread, that is, the SimpleDateFormat instance is reused in the thread dimension , and in pooling scenarios such as thread pools, it is repaired compared to the closure of the upper stack The scheme reduces GC loss and avoids thread safety issues.

The above use of ThreadLocal to reuse non-thread-safe instances in the thread dimension can be considered as a general pattern , and similar patterns can be seen in JDK and many open source projects. For example, in the most common String class of JDK, for characters The StringDecoder and StringEncoder needed to encode and decode strings use ThreadLocal to avoid thread safety issues:

/**
 * Utility class for string encoding and decoding.
 */
class StringCoding {

    private StringCoding() { }

    /** The cached coders for each thread */
    private final static ThreadLocal<SoftReference<StringDecoder>> decoder =
        new ThreadLocal<>();
    private final static ThreadLocal<SoftReference<StringEncoder>> encoder =
        new ThreadLocal<>();

    // ...
}


Reference: JDK8 - String Coding

In Dubbo's ThreadLocalKryoFactory class, in the use of the non-thread-safe class Kryo, the ThreadLocal class is also used to avoid thread safety issues:

package org.apache.dubbo.common.serialize.kryo.utils;

import com.esotericsoftware.kryo.Kryo;

public class ThreadLocalKryoFactory extends AbstractKryoFactory {

    private final ThreadLocal<Kryo> holder = new ThreadLocal<Kryo>() {
        @Override
        protected Kryo initialValue() {
            return create();
        }
    };

    @Override
    public void returnKryo(Kryo kryo) {
        // do nothing
    }

    @Override
    public Kryo getKryo() {
        return holder.get();
    }
}


Reference: Dubbo - ThreadLocalKryoFactory

Similarly, in the ConcurrentBag class of HikariCP, the ThreadLocal class is also used to avoid thread safety issues, and will not be further expanded here.

Solution 4: FastDateFormat (recommended)

For the thread safety problem of SimpleDateFormat class, apache commons-lang provides FastDateFormat class. Part of its Java Doc is as follows:

FastDateFormat is a fast and thread-safe version ofSimpleDateFormat. To obtain an instance of FastDateFormat, use one of the static factory methods:getInstance(String, TimeZone, Locale),getDateInstance(int, TimeZone, Locale),getTimeInstance(int, TimeZone, Locale), orgetDateTimeInstance(int, int, TimeZone, Locale) Since FastDateFormat is thread safe, you can use a static member instance: private static final FastDateFormat DATE_FORMATTER = FastDateFormat.getDateTimeInstance(FastDateFormat.LONG, FastDateFormat.SHORT); This class can be used as a direct replacement toSimpleDateFormatin most formatting and parsing situations. This class is especially useful in multi-threaded server environments.SimpleDateFormatis not thread-safe in any JDK version, nor will it be as Sun have closed the bug/RFE. All patterns are compatible with SimpleDateFormat (except time zones and some year patterns - see below).

This repair solution is relatively minimal in code modification, and only needs to replace the SimpleDateFormat instance with the FastDateFormat instance at the place where the static SimpleDateFormat instance code is declared. The sample code is as follows:

package com.jd.threadsafe;

import org.apache.commons.lang3.time.FastDateFormat;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @Date: 2023/7/6 20:05
 * @Desc: SimpleDateFormat 线程安全修复方案:FastDateFormat
 * @Version: V1.0
 **/
public class FastDateFormatTest {
    private static final AtomicBoolean STOP = new AtomicBoolean();
    private static final FastDateFormat FORMATTER = FastDateFormat.getInstance("yyyy-M-d");

    public static void main(String[] args) {
        Runnable runnable = () -> {
            int count = 0;
            while (!STOP.get()) {
                try {
                    FORMATTER.parse("2023-7-15");
                } catch (Exception e) {
                    e.printStackTrace();
                    if (++count > 3) {
                        STOP.set(true);
                    }
                }
            }
        };

        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}


Executing the above code will no longer observe abnormal output, because FastDateFormat is a thread-safe implementation that supports multi-threaded concurrent calls.

Summarize

No matter which repair solution is used, it needs to be fully tested after the modification to ensure that the original business logic will not be affected after the repair, such as unit testing, traffic playback, etc. to ensure the correctness of this repair.

think

The reason why the SimpleDateFormat class is used in the code is because the date uses the Date class, and the JDK formatting class that matches the Date is the SimpleDateFormat class. If we use the LocalDateTime and other immutable date classes introduced by JDK8 when dealing with dates, then the formatting will use The supporting thread-safe DateTimeFormatter class avoids the use of the non-thread-safe SimpleDateFormat class from the root.

Author: JD Logistics Liu Jianshe Zhang Jiulong Tian Shuang

Source: Reprinted from Yuanqishuo Tech by JD Cloud developer community, please indicate the source

The Indian Ministry of Defense self-developed Maya OS, fully replacing Windows Redis 7.2.0, and the most far-reaching version 7-Zip official website was identified as a malicious website by Baidu. Go 2 will never bring destructive changes to Go 1. Xiaomi released CyberDog 2, More than 80% open source rate ChatGPT daily cost of about 700,000 US dollars, OpenAI may be on the verge of bankruptcy Meditation software will be listed, founded by "China's first Linux person" Apache Doris 2.0.0 version officially released: blind test performance 10 times improved, More unified and diverse extremely fast analysis experience The first version of the Linux kernel (v0.01) open source code interpretation Chrome 116 is officially released
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/10098343