Uso detalhado de SimpleDateFormat em Java

No desenvolvimento diário, geralmente usamos o tempo e temos muitas maneiras de obter tempo no código Java. No entanto, o formato da hora obtida por diferentes métodos é diferente. Neste momento, é necessária uma ferramenta de formatação para exibir a hora no formato que precisamos.

A maneira mais comum é usar a classe SimpleDateFormat. Essa é uma classe que parece ter funções relativamente simples, mas se for utilizada de forma inadequada pode causar sérios problemas.

No Alibaba Java Development Manual, existem regulamentos claros como segue:

Então, este artigo enfoca o uso e o princípio do SimpleDateFormat para analisar como usá-lo na postura correta.

Uso do SimpleDateFormat

SimpleDateFormat é uma classe de ferramenta fornecida pelo Java para formatação e análise de datas. Permite formatação (data -> texto), análise (texto -> data) e normalização. SimpleDateFormat permite a seleção de qualquer padrão de formato de data e hora definido pelo usuário.

Em Java, você pode usar o método format de SimpleDateFormat para converter um tipo Date em um tipo String e pode especificar o formato de saída.

// Date转String
Date data = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dataStr = sdf.format(data);
System.out.println(dataStr);

O código acima, o resultado da conversão é: 2018-11-25 13:00:00, o formato de data e hora é especificado pela string "padrão de data e hora". Se você deseja converter para outro formato, basta especificar um modo de hora diferente.

Em Java, você pode usar o método parse de SimpleDateFormat para converter um tipo String em um tipo Date.

// String转Data
System.out.println(sdf.parse(dataStr));

Método de expressão de padrão de data e hora

Ao usar SimpleDateFormat, você precisa descrever os elementos de hora por letras e montá-los nos padrões de data e hora desejados. A tabela de correspondência entre elementos de tempo e letras comumente usados ​​é a seguinte:

![-w717][1]

As letras padrão geralmente são repetidas, cujo número determina sua representação exata. A tabela a seguir mostra os formatos de saída comumente usados.

![-w535][2]

Tempo de saída em diferentes fusos horários

Um fuso horário é uma área do globo que usa a mesma definição de tempo. Antigamente, as pessoas determinavam a hora observando a posição do sol (ângulo horário), o que tornava diferentes as horas de lugares com diferentes longitudes (hora local). Em 1863, o conceito de fuso horário foi usado pela primeira vez. Os fusos horários resolvem esse problema em parte definindo um horário padrão para uma região.

Os países do mundo estão localizados em diferentes posições na Terra, portanto, os horários do nascer e do pôr do sol de diferentes países, especialmente aqueles com uma grande extensão leste-oeste, devem ser diferentes. Esses desvios são conhecidos como jet lag.

Hoje o mundo está dividido em 24 fusos horários. Na prática, um país ou uma província geralmente abrange dois ou mais fusos horários ao mesmo tempo. Para cuidar da conveniência administrativa, um país ou uma província geralmente é agrupado. Portanto, o fuso horário não é estritamente dividido pela linha reta norte-sul, mas pelas condições naturais. Por exemplo, a China tem um vasto território e abrange quase 5 fusos horários, porém, para comodidade e simplicidade de uso, na verdade, prevalece apenas o horário padrão do 8º fuso horário Leste, ou seja, o horário de Pequim.

Como o horário em diferentes fusos horários é diferente, mesmo em cidades diferentes do mesmo país, o horário pode ser diferente, portanto, quando você deseja obter o horário em Java, deve prestar atenção ao fuso horário.

Por padrão, se não for especificado, o fuso horário onde o computador atual está localizado será usado como fuso horário padrão ao criar a data, e é por isso que podemos obter a hora atual na China apenas usando-o new Date().

Então, como obter tempo em fuso horário diferente no código Java? SimpleDateFormat pode alcançar esta função.

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles"));
System.out.println(sdf.format(Calendar.getInstance().getTime()));

O código acima, o resultado da conversão é: 2018-11-24 21:00:00 . O horário na China é 13:00 do dia 25 de novembro, e o horário em Los Angeles é 16 horas atrás do horário de Pequim na China (isso também está relacionado ao inverno e ao verão, então não vou entrar em detalhes).

Se você estiver interessado, também pode tentar imprimir a hora em Nova York, EUA (America/New_York). O horário de Nova York é 25/11/2018 00:00:00. O horário de Nova York está 13 horas adiantado em relação ao horário de Pequim na China.

Claro, esta não é a única maneira de exibir outros fusos horários, mas este artigo apresenta principalmente o SimpleDateFormat, e outros métodos não serão introduzidos por enquanto.

Segurança de encadeamento SimpleDateFormat

Como SimpleDateFormat é comumente usado e, em geral, o modo de exibição de hora em um aplicativo é o mesmo, muitas pessoas estão dispostas a definir SimpleDateFormat da seguinte maneira:

public class Main {

    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        simpleDateFormat.setTimeZone(TimeZone.getTimeZone("America/New_York"));
        System.out.println(simpleDateFormat.format(Calendar.getInstance().getTime()));
    }
}

Este método de definição tem grandes riscos de segurança.

reprodução de problemas

Vejamos um pedaço de código, o código a seguir usa o pool de threads para executar a saída de tempo.

   /** * @author Hollis */ 
   public class Main {

    /**
     * 定义一个全局的SimpleDateFormat
     */
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    /**
     * 使用ThreadFactoryBuilder定义一个线程池
     */
    private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
        .setNameFormat("demo-pool-%d").build();

    private static ExecutorService pool = new ThreadPoolExecutor(5, 200,
        0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>(1024), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());

    /**
     * 定义一个CountDownLatch,保证所有子线程执行完之后主线程再执行
     */
    private static CountDownLatch countDownLatch = new CountDownLatch(100);

    public static void main(String[] args) {
        //定义一个线程安全的HashSet
        Set<String> dates = Collections.synchronizedSet(new HashSet<String>());
        for (int i = 0; i < 100; i++) {
            //获取当前时间
            Calendar calendar = Calendar.getInstance();
            int finalI = i;
            pool.execute(() -> {
                    //时间增加
                    calendar.add(Calendar.DATE, finalI);
                    //通过simpleDateFormat把时间转换成字符串
                    String dateString = simpleDateFormat.format(calendar.getTime());
                    //把字符串放入Set中
                    dates.add(dateString);
                    //countDown
                    countDownLatch.countDown();
            });
        }
        //阻塞,直到countDown数量为0
        countDownLatch.await();
        //输出去重后的时间个数
        System.out.println(dates.size());
    }
}

O código acima é relativamente simples e fácil de entender. É fazer um loop cem vezes e adicionar um número de dias ao horário atual toda vez que ele fizer um loop (esse número de dias muda com o número de loops) e, em seguida, colocar todas as datas em um Conjunto seguro para thread com função de desduplicação e, em seguida, imprima o número de elementos no conjunto.

O exemplo acima que escrevi propositalmente é um pouco mais complicado, mas adicionei comentários quase todo. Isso envolve [criação de pool de threads][3], [CountDownLatch][4], expressões lambda, HashSet seguro para threads e outros conhecimentos. Amigos interessados ​​podem descobrir um por um.

Em circunstâncias normais, a saída do código acima deve ser 100. Mas o resultado real da execução é um número menor que 100.

A razão é que SimpleDateFormat, como uma classe não thread-safe, é usada como uma variável compartilhada em vários threads, o que leva a problemas de segurança de thread.

Há também uma declaração clara sobre este ponto no Capítulo 1, Seção 6 do Alibaba Java Development Manual - Processamento Simultâneo:

Então, vamos dar uma olhada no porquê e como resolvê-lo.

motivo inseguro do tópico

Por meio do código acima, descobrimos que usar SimpleDateFormat em cenários simultâneos terá problemas de segurança de thread. Na verdade, a documentação do JDK afirma claramente que SimpleDateFormat não deve ser usado em cenários multiencadeados:

Os formatos de data não são sincronizados. É recomendável criar instâncias de formato separadas para cada thread. Se vários threads acessarem um formato simultaneamente, ele deverá ser sincronizado externamente.

Em seguida, analise por que esse tipo de problema ocorre e como a camada inferior do SimpleDateFormat é realizada?

Vamos acompanhar a implementação do método format na classe SimpleDateFormat para descobrir.

![][5]

O método de formato em SimpleDateFormat usará um calendário variável de membro para salvar o tempo durante a execução. Este é realmente o cerne do problema.

Porque quando declaramos SimpleDateFormat, usamos a definição estática. Então, este SimpleDateFormat é uma variável compartilhada e, em seguida, o calendário em SimpleDateFormat pode ser acessado por vários encadeamentos.

Suponha que a thread 1 acabou de terminar a execução calendar.setTimee defina a hora para 11/11/2018. Antes de terminar a execução, a thread 2 executa novamente calendar.setTimee altera a hora para 12/12/2018. Neste momento, o thread 1 continua a executar e calendar.getTimeo tempo obtido é após a alteração do thread 2.

Além do método format, o método parse de SimpleDateFormat tem o mesmo problema.

Portanto, não use SimpleDateFormat como uma variável compartilhada.

Como resolver

Eu apresentei os problemas do SimpleDateFormat e os motivos dos problemas, então existe alguma maneira de resolver esse problema?

Existem muitas soluções, aqui estão três métodos mais comumente usados.

usar variáveis ​​locais

for (int i = 0; i < 100; i++) {
    //获取当前时间
    Calendar calendar = Calendar.getInstance();
    int finalI = i;
    pool.execute(() -> {
        // SimpleDateFormat声明成局部变量
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        //时间增加
        calendar.add(Calendar.DATE, finalI);
        //通过simpleDateFormat把时间转换成字符串
        String dateString = simpleDateFormat.format(calendar.getTime());
        //把字符串放入Set中
        dates.add(dateString);
        //countDown
        countDownLatch.countDown();
    });
}

SimpleDateFormat torna-se uma variável local, portanto, não será acessado por vários threads ao mesmo tempo, evitando problemas de segurança do thread.

Adicionar bloqueio de sincronização

Além de mudar para variáveis ​​locais, existe outro método com o qual você pode estar mais familiarizado, que é bloquear variáveis ​​compartilhadas.

for (int i = 0; i < 100; i++) {
    //获取当前时间
    Calendar calendar = Calendar.getInstance();
    int finalI = i;
    pool.execute(() -> {
        //加锁
        synchronized (simpleDateFormat) {
            //时间增加
            calendar.add(Calendar.DATE, finalI);
            //通过simpleDateFormat把时间转换成字符串
            String dateString = simpleDateFormat.format(calendar.getTime());
            //把字符串放入Set中
            dates.add(dateString);
            //countDown
            countDownLatch.countDown();
        }
    });
}

Ao bloquear, vários threads são enfileirados e executados sequencialmente. Evita problemas de segurança de encadeamento causados ​​por simultaneidade.

Na verdade, ainda há espaço para melhorias no código acima, ou seja, a granularidade do bloqueio pode ser menor e apenas simpleDateFormat.formatesta bloqueada, o que é mais eficiente.

Usar ThreadLocal

A terceira maneira é usar ThreadLocal. ThreadLocal pode garantir que cada thread possa obter um único objeto SimpleDateFormat, portanto, naturalmente, não haverá problemas de concorrência.

/**
 * 使用ThreadLocal定义一个全局的SimpleDateFormat
 */
private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
};

//用法
String dateString = simpleDateFormatThreadLocal.get().format(calendar.getTime());

Usar ThreadLocal para implementar é realmente um pouco semelhante à ideia de cache. Cada thread possui um objeto exclusivo, o que evita a criação frequente de objetos e a competição multi-thread.

Obviamente, o código acima também pode ser aprimorado, ou seja, o processo de criação de SimpleDateFormat pode ser alterado para carregamento lento. Não vou entrar em detalhes aqui.

Usar DateTimeFormatter

Se for um aplicativo Java8, você pode usar DateTimeFormatter em vez de SimpleDateFormat, que é uma classe de ferramenta de formatação thread-safe. Conforme declarado na documentação oficial, essa classe é simples, bonita, forte e imutável com thread-safe.

//解析日期
String dateStr= "2016年10月25日";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");
LocalDate date= LocalDate.parse(dateStr, formatter);

//日期转换为字符串
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a");
String nowStr = now .format(format);
System.out.println(nowStr);

Resumir

Este artigo apresenta o uso de SimpleDateFormat. SimpleDateFormat pode converter principalmente entre String e Date, e também pode converter tempo em diferentes fusos horários para saída. Ao mesmo tempo, é mencionado que SimpleDateFormat não pode garantir a segurança do encadeamento em cenários simultâneos e os desenvolvedores precisam garantir sua segurança.

Os métodos principais estão mudando para variáveis ​​locais, usando sincronizado para bloquear, usando Threadlocal para criar um thread separado para cada thread, etc.

Espero que, por meio deste artigo, você possa ser mais prático ao usar SimpleDateFormat.

Acho que você gosta

Origin blog.csdn.net/zy_dreamer/article/details/132307231
Recomendado
Clasificación