Java 中 Calendar 类的方法是线程安全的吗

发布于:4/25/2023, 10:27:34 AM @孙博
技术分享 | Java,Calendar,线程安全
许可协议:署名-非商业性使用(by-nc)

众所周知,与很多语言不一样的是,在“时间”对象的处理中,Java 比很多语言都要麻烦一些。假如在不借助时间戳计算的前提下,我们如果需要对一个日期做加减,比如获取昨天的日期这种类似的需求,往往还需要借助第三方来实现。

下面给出一个常见的例子。

import java.util.Calendar;
import java.util.Date;

public class YesterdayDate {
    public static void main(String[] args) {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DATE, -1); // 向前推一天
        Date yesterday = calendar.getTime(); // 获取昨天的时间
        System.out.println(yesterday);
    }
}

没错,虽然比起其他语言来说,代码多了些,步骤多了些,但是确实是可以满足我们的要求。可当我们在代码中看到 getInstance 时就需要多想一下,这个方法名不是我们在构造单例时经常会用到的吗?可假如是单例的方法,那么它还会是线程安全的吗?

带着这个问题我们尝试借助当下最火的 ChatGPT 给出答案。以下为对话记录:


来自你的消息:

Java怎么获取昨天的时间,数据类型为 java.util.Date

来自 ChatGPT 的消息:

获取昨天的时间,可以通过以下代码实现:

import java.util.Calendar;
import java.util.Date;

public class YesterdayDate {
    public static void main(String[] args) {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DATE, -1); // 向前推一天
        Date yesterday = calendar.getTime(); // 获取昨天的时间
        System.out.println(yesterday);
    }
}

这里利用了java.util.Calendar类中的add方法把日期向前推一天,再利用getTime方法获取Date类型的昨天时间对象。另外,需要注意的是,Date类型的对象本身并不包含时区信息,但是可以认为是以UTC(世界标准时间)时区的时间进行计算的。如果需要显示一定的时区信息,还需要结合SimpleDateFormat格式化输出。

来自你的消息:

Calendar 是线程安全的吗?还是说我需要在每一次调用过程中都重新获取一次?

来自 ChatGPT 的消息:

Calendar并不是线程安全的类,多线程环境下应当避免共享同一个Calendar对象实例。否则可能会出现并发问题,导致结果不确定或不符合预期。

有两种方式可以解决多线程环境下的线程安全问题:

  1. 在每次使用Calendar对象实例的时候,都创建一个新的实例,这种方式所导致的额外开销较大。

  2. 使用ThreadLocal来存储Calendar实例,这种方式可以避免多线程并发访问时出现的线程安全问题。下面是一个使用ThreadLocal的示例:

   import java.util.Calendar;

   public class ThreadLocalCalendar {
       private static final ThreadLocal<Calendar> calendarThreadLocal = new ThreadLocal<Calendar>() {
           protected Calendar initialValue() {
               return Calendar.getInstance();
           }
       };

       public static Calendar getCalendar() {
           return calendarThreadLocal.get();
       }

       public static void main(String[] args) {
           Calendar calendar = getCalendar();
           // .. do something
       }
   }

在这个代码示例中,使用了一个ThreadLocal来存储每个线程所使用的Calendar实例,可以保证每个线程使用的实例都是线程私有的。在实际使用过程中,可以通过getCalendar方法来获取当前线程持有的Calendar实例。

综上所述,为了保证多线程环境下程序的正确性和性能,可以采用第二种方式,在使用Calendar的地方,通过ThreadLocal来存储线程私有的实例。同时,需要注意到线程池和异步编程等技术方案也可能会对多线程下Calendar的使用造成影响,需要根据具体情况进行解决。

来自你的消息:

通过 Calendar.getInstance() 生成的对象是线程安全的吗

来自 ChatGPT 的消息:

Calendar.getInstance()生成的对象并不是线程安全的,因为它在内部是单例模式(Singleton)的实现,所有线程都共享同一个实例。同时,Calendar对象内部状态是可变的(例如调用add等方法会修改其内部状态),所以如果多个线程同时访问同一个Calendar对象,就会出现并发问题,导致结果不确定或不符合预期。

因此,在多线程环境下,每次使用Calendar时都应该重新获取一个新实例,避免多个线程之间的竞争和修改,从而确保线程安全性。可以使用Calendar.getInstance(TimeZone timezone, Locale locale)方法手动创建新的实例,也可以使用ThreadLocal来存储线程私有的Calendar实例,保证线程安全和性能。

来自你的消息:

如何使用线程安全的方法获取昨天的日期对象

来自 ChatGPT 的消息:

如果需要获取昨天的Date对象,并且希望获取的对象是线程安全的,可以使用java.time包中的日期时间类进行操作,例如使用LocalDateTimeZoneId来获取昨天的日期对象,然后通过Date.from方法转换成线程安全的Date对象,示例如下:

import java.time.*;
import java.util.Calendar;
import java.util.Date;

public class SafeYesterdayDate {

    public static void main(String[] args) {
        LocalDateTime yesterday = LocalDateTime.now().minusDays(1);
        ZoneId systemZone = ZoneId.systemDefault();
        Instant instant = yesterday.atZone(systemZone).toInstant();
        Date safeYesterday = Date.from(instant);
        System.out.println(safeYesterday);
    }

}

这个程序首先使用LocalDateTime类获取当前时间的前一天时间对象,然后使用ZoneId.systemDefault()方法获取系统默认时区,将LocalDateTime转换为带时区的ZonedDateTime对象,再通过toInstant方法获取瞬时时间对象,最后使用Date.from方法将Instant对象转换成线程安全的Date对象。

由于java.time包中的日期时间类都是线程安全的,因此通过这种方式获取日期对象可以避免多线程环境下可能出现的并发问题。这也是从Java 8开始,建议使用java.time包中的日期时间类来替代java.util.Datejava.util.Calendar等传统的日期时间类的原因之一。