如何使静态日历线程安全

10

我想使用一个日历来实现一些静态方法并使用一个静态字段:

private static Calendar calendar = Calendar.getInstance();

我了解到java.util.Calendar不是线程安全的。如何使它线程安全(它应该是静态的)?


你的静态日历只是一个固定日期吗? - toto2
也许将其声明为“volatile”会有帮助? - cody
不,我只是不想为每次调用我的方法创建那么多实例,因为那可能会花费时间。 - cody
你的意思是你担心多次调用构造函数会影响性能? - toto2
是的,至少目前不行,因为这段代码是针对移动设备的。 - cody
2
我开始得出结论,日历不应该仅仅被视为“非线程安全”。它应该被标记为“线程敌对”。 - scottb
6个回答

14
如果某个东西本身不是线程安全的,你就无法使它变得线程安全。在 Calendar 的情况下,即使仅从中读取数据,也不是线程安全的,因为它可能会更新内部数据结构。
如果可能的话,我建议使用 Joda Time
  • 大多数类型都是不可变的
  • 这些不可变类型是线程安全的
  • 总的来说,这是一个更好的 API
如果你绝对必须使用 Calendar,你可以创建一个锁对象,并通过锁来访问所有的操作。例如:
private static final Calendar calendar = Calendar.getInstance();
private static final Object calendarLock = new Object();

public static int getYear()
{
    synchronized(calendarLock)
    {
        return calendar.get(Calendar.YEAR);
    }
}

// Ditto for other methods

不过这种方式相当恶劣。当然,你可以只编写一个同步方法,每次需要它时都创建原始日历的克隆副本......当然,通过调用computeFieldscomputeTime,你可以使后续的读操作线程安全,但就我个人而言,我不愿尝试。


1
@Peter:想象一下,如果在另一个线程中计算后,areFieldsSet变成可见的true,而计算出的字段值本身还没有变得可见,将会发生什么。这可能导致一个线程读取一些旧字段值和一些新字段值,从而导致奇怪的结果。我不喜欢尝试推断实现细节来决定是否可能发生这种情况。 - Jon Skeet
@bestsss 你必须承认,日历API的设计并不是最好的(从月份从0开始,日期从1开始等琐碎的事情开始),这使得“简单”的事情比应该更难。再加上在这种情况下,一个不可变类更容易使用 - 我同意。 - Voo
1
@Voo:我的闹钟可以每天设定在当地时间8点叫醒我,无论我身在何处。它的表示应该使用哪个时区? ;) - Jon Skeet
@Jon,某个特定日期的某事 - 永远不只是时间或任何本地时区等。它始终是人对时间的想象,必须在某个时刻内部表示(即应用日期/年份+时区)。一旦缺失信息可用(日期+时区),有效时间也将随之可用。如何存储先前时间(小时数、分钟数、毫秒数或仅一个包含所有信息的单个整数-自午夜以来的毫秒数)并不是一个重要问题,例如,我会编写一个类来存储它,直到可以表示为实际时间(基于UTC),在那时候抓取一个日历并设置它->long - bestsss
@bestsss:如果你想表示的只是一个时间点,那么这很好并且具有可移植性。对于时间戳这样的东西来说没问题,但是对于像重复事件这样总是在特定时区相同时间的东西来说就不行了。试图强制将许多不同的概念放入同一类型是Java和.NET在我看来犯的错误。处理日期和时间已经够困难了,不必要再与一个糟糕的API斗争:( - Jon Skeet
显示剩余27条评论

3

日历是线程安全的,只要你不改变它。在你的例子中使用它是可以的。

值得注意的是,日历不是一个高效的类,你应该只在复杂操作(比如查找下个月/年)时使用它。如果你确实要使用它进行复杂操作,请仅使用局部变量。

如果你只想要时间的快照,更快的方法是使用currentTimeMillis,它甚至不会创建对象。如果你想使它线程安全,可以将字段设置为volatile。

private static long now = System.currentTimeMillis();

这种用法有些可疑。为什么要获取当前时间并在全局范围内存储它呢?这让我想起了一个老笑话:

- 你知道现在几点吗?
- 是的,我在某个地方写下了它。


2
考虑到从中读取可能会更改其内部状态,"更改它"确切意味着什么?我不会完全惊讶地发现两个同时读取操作可能会潜在地损坏状态。这不太可能发生,但我不希望依赖它。 Calendar是一个讨厌的、讨厌的类:( - Jon Skeet
阅读它不会以不一致的方式改变其状态。如果您担心这个问题,那么您也必须对String.hashCode()说同样的话。 - Peter Lawrey
我同意Calendar很糟糕。你试过序列化它吗?它会发送整个时区信息,这对性能不是很好,但这意味着如果您的系统具有最新的时区信息,但您读取旧的日历或从未更新时区信息的系统获取日历,则由于您未运行的代码中的错误,您将遇到意想不到的微妙时区问题。 :P - Peter Lawrey
4
请看我在回答中回复您的评论。String 设计为线程安全,因此其哈希缓存也是线程安全的:该值从“未缓存”原子性地更改为“已缓存的值”。与此相比,Calendar 中有一个布尔字段确定是否设置了字段,并且字段本身是分离的。什么保证对该数据集的所有更改是以原子方式进行的,或者以期望的顺序进行的? - Jon Skeet
我还有一个数据库应用程序,其中日期/时间以UTC数值格式存储。每次需要向用户显示日期时,该表的代码都需要一种从UTC数字到本地化字符串的映射方式。为此,我一直在使用Calendar,因为TimeZone提供了历史准确的夏令时和时区偏移量。由于我有很多需要进行此转换的类,并且创建日历非常昂贵,因此我还在我的JulianDay类中使用一个静态日历字段来执行日期本地化。真正苦恼如何使这个API线程安全。 - scottb
1
@scottb Calendar变慢的一部分原因是创建TimeZone和Locale对象。如果您缓存这些不可变对象,可以加快创建新日历的速度。为了使Calendar线程安全,您需要在使用它时对其进行锁定。 - Peter Lawrey

3

简而言之

CalendarZonedDateTime取代。 不可变性,因此线程安全

private static ZonedDateTime someMoment = ZonedDateTime.now();  // Capture the current moment as seen in the JVM’s current default time zone.

如果这个值在你的应用程序执行期间是固定不变的,那么请将其标记为final
private static final ZonedDateTime someMoment = ZonedDateTime.now();  

如果在运行时可能分配不同的对象,那么为了线程安全,请使用AtomicReference
private static final AtomicReference< ZonedDateTime > someMomentRef = new AtomicReference<>( ZonedDateTime.now() ) ;

稍后更新,敬请期待。
someMomentRef.set( ZonedDateTime.now() );

如果你需要与尚未更新到java.time的旧代码进行互操作,进行转换。
ZonedDateTime someMoment = someMomentRef.get() ;
Calendar c = GregorianCalendar.from( someMoment ) ;  // `GregorianCalendar` is a concrete subclass of `Calendar`.

java.time.ZonedDateTime

可怕有缺陷的 java.util.Calendar 类已被定义在JSR 310中的现代java.time类所取代。具体被java.time.ZonedDateTime替代。

ZonedDateTime对象表示在特定时区中看到的带时间的日期。

了解时区是一个由他们的政治家决定的特定地区居民使用的偏移更改的历史名称。偏移仅仅是比UTC的时间子午线提前或推后的小时-分钟-秒数。

要获取当前时刻,请指定您所期望的时区。

ZoneId z = ZoneId.of( "Africa/Casablanca" ) ;
ZonedDateTime zdt = ZonedDateTime.now( z ) ;

如果您想获取JVM的当前默认时区,请使用ZoneId.systemDefault()

您可以将ZonedDateTime对象存储在static引用中。但请注意,这样的对象存储了一个固定的时刻,是不可变的,并且不会调整到以后的时刻。如果您想要以后的时刻,请调用now方法生成一个新的ZonedDateTime对象。

private static final ZonedDateTime appLaunched = ZonedDateTime.now( ZoneId.of( "Asia/Tokyo" ) );

java.time.Instant

如果你只是想追踪一个瞬间,时间线上的特定点,而没有明确针对时区的业务规则,请使用以UTC为基准的瞬间(moment)。为此,使用java.time.Instant类。

private static final Instant instant = Instant.now() ;  // Always in UTC, an offset of zero.

生成本地化文本
您以后可以为用户应用时区进行展示。
ZonedDateTime zdt = instant.atZone( ZoneId.systemDefault() ) ;
String output = 
    zdt
    .atZone( ZoneId.systemDefault() )  // Returns a `ZonedDateTime` object.
    .format( 
        DateTimeFormatter
        .ofLocalizedDateTime( FormatStyle.FULL )  // Returns a `DateTimeFormatter` object.
        .withLocale( Locale.getDefault() )
    ) ;  // Returns a `String` object.

2
你不能这样做。是的,你可以在其上同步,但它仍然具有可变状态字段。你必须创建自己的日历对象。
如果可能,请使用轻量级的东西,比如以毫秒为单位测量时间,并且只在需要时转换为日历对象。

2

不要试图使Calendar API线程安全,而是使用Java 8中引入的线程安全的java.time API。该API还提供了更流畅和直观的日期和时间操作界面。

以下是一个与传统Date API兼容的示例,使用java.time API获取下一天的开始:

import java.time.LocalDateTime;
import java.time.ZoneId;

public static Date getStartOfNextDay() {
    final ZonedDateTime startOfNextDay = ZonedDateTime.now()
            .plusDays(1)
            .withHour(0).withMinute(0).withSecond(0).withNano(0);
    return Date.from(startOfNextDay.toInstant());
}

这是使用Calendar API的效果:
import java.util.Calendar;

public static Date getStartOfNextDay() {
    final Calendar calendar = Calendar.getInstance();
    calendar.add(Calendar.DAY_OF_MONTH, 1);
    calendar.set(Calendar.HOUR_OF_DAY, 0);
    calendar.set(Calendar.MINUTE, 0);
    calendar.set(Calendar.SECOND, 0);
    calendar.set(Calendar.MILLISECOND, 0);
    return calendar.getTime();
}

2
我不明白在这段代码中使用LocalDateTime有什么价值。替代java.util.Calendar的是java.time.ZonedDateTime - Basil Bourque
@BasilBourque,谢谢,你是对的,已修复! - mrts
@BasilBourque,在仔细看了一遍后,我发现我的回答无意中与你的回答相同。你觉得我应该删除我的回答,支持你的回答吗? - mrts
1
不用管它。你已经得到了一个赞。提供一个替代的例子并不会有坏处。而且你的回答也启发了我。我建议你在代码中加入一些解释。 - Basil Bourque
谢谢你的建议!代码片段中的API看起来非常易读流畅,而且代码本身似乎也很容易理解——你觉得呢? - mrts

-1
在方法中创建一个本地变量作为Calendar。如果您需要在多个方法中使用相同的日历,则可以使用静态变量,其中单例或准单例对象更合适。

4
你真的在建议将可变、非线程安全对象制作成单例来帮助使其类线程安全吗?真的吗? - scottb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接