【问题标题】:Java: How to make an application-wide GregorianCalendar thread safe?Java:如何使应用程序范围的 GregorianCalendar 线程安全?
【发布时间】:2013-06-06 18:12:28
【问题描述】:

我有一个包含许多表的数据库,这些表以 UTC 数字格式(修改后的儒略日数字)存储日期/时间。

这很好用,但当然需要向用户显示任何时间日期,日期/时间必须从 UTC 数字格式转换为本地化字符串格式。因为 JDK 日历 API 提供了历史上准确的夏令时和时区偏移,所以我一直使用公历来实现这个唯一目的。我只使用它来将时间戳从 UTC 映射到本地时间。

因为我有很多表需要执行这种转换,而且创建日历对象的成本很高,所以我只在语言环境时区更改时才创建新的 GregorianCalendar 对象。我一直将当前日历保存在 JulianDay 类的静态成员字段中(该类提供了我用来提供 UTC 到本地映射的函数)。以下是 JulianDay 的字段:

public final class JulianDay {

    private static final int YEAR = 0;
    private static final int MONTH = 1;
    private static final int DAY = 2;
    private static final int HOURS = 3;
    private static final int MINUTES = 4;
    private static final int SECONDS = 5;
    private static final int MILLIS = 6;

    public  static final double POSIX_EPOCH_MJD = 40587.0;
    private static final String TIME_ZONE_UTC   = "UTC";
    private static final GregorianCalendar CAL_UTC = 
            new GregorianCalendar(TimeZone.getTimeZone(TIME_ZONE_UTC));

    private static GregorianCalendar c_selCal = null;

    public static void setCurrentCalendar(String tzid) 
            { JulianDay.c_selCal = JulianDay.findCalendarForTimeZone(tzid); }

    public static GregorianCalendar getCurrentCalendar() 
            { return JulianDay.c_selCal; }
    :
    :
}

以上是在单线程客户端 GUI 应用程序中,但我很快需要开发一个服务器端 Web 应用程序,它将使用该 API 的大部分内容。我需要弄清楚如何保留我对 JDK 日历 API 的投资,并以某种方式保证我的应用程序线程安全。 Joda time 是一个选项,但如果可能的话,我不想添加该依赖项(无论如何,我使用 Java API 只是简单模式访问时区数据库)。

由于日历显然是线程敌对的,我不确定如何从这里开始。

我的想法是,由于 JulianDay 类构造了 GregorianCalendar 的所有实例(来自时区 id 字符串),我可以重构我的代码,这样我就可以消除发布日历引用的 getCurrentCalendar() 方法。

如果我可以将所有对GregorianCalendar 的访问限制在JulianDay 内,我是否可以安全地假设,即使它是一个危险的可变类,我的应用程序也是线程安全的?即使在我的JulianDay 类中,我仍然需要将对GregorianCalendar 的访问与锁定对象同步,对吗?

【问题讨论】:

  • 也许使用ThreadLocal<Calendar>

标签: java calendar thread-safety


【解决方案1】:

您可以使用 Thread 本地静态成员变量,例如:

private static final ThreadLocal<GregorianCalendar> CAL_UTC = 
        new ThreadLocal<GregorianCalendar>() {
         @Override protected GregorianCalendar initialValue() {
             return  new GregorianCalendar(TimeZone.getTimeZone(TIME_ZONE_UTC));
     }
 };

这样每个线程都有一个对象。更多信息请查看ORACLE documentation

希望这会有所帮助……干杯!

p.s.:使用这种方法需要考虑两个方面:

  1. 线程局部变量的目的是为不同的线程提供不同的值。如果您需要跨线程 使用相同的日历,这种方法可能不适合您。
  2. 请注意不要意外收集您的日历(请参阅 ThreadLocal Resource Leak and WeakReference) - 因此请在 ThreadLocal 实例上保留强引用。

如果您需要访问实例 accross 线程,您必须派生自己的类并同步相关方法或寻找替代实现(检查实例 Apache commons)。

【讨论】:

  • ThreadLocal ...我应该能够看到它的到来。这似乎特别。适合 CAL_UTC,因为它是一个静态最终字段,旨在成为 UTC 映射的“常量”日历。 c_selCal 旨在设置为适合当前选定数据库记录的日历,即它的引用和可变的。在 ThreadLocal 中它仍然安全吗?
  • +1 这是个好问题。我认为只要您在initialValue() 中创建一个新实例就可以了,但有一些事情需要考虑(见上文)。
【解决方案2】:

如果正确同步,可变实例可以是线程安全的。不可变实例的优势在于您不需要同步。所以是的,如果您将所有变异更改同步到日历,您可以继续使用它。

但是!服务器端应用程序与多线程客户端应用程序完全不同。单点同步是阻止应用程序的可靠方法 - 根据负载,连接可能会停止。虽然 ThreadLocal 可能会缓解同步问题,但由于每个日历仅在单个线程中可见,请考虑线程重用。

在典型的应用服务器环境中,相同的线程不仅会被连续的用户重用,而且会在同一个会话中被重用——为了控制和保存服务器资源,每个 GET 都是一个工作单元,由单个线程尽快为其提供服务随着工作单元完成,同一个线程可以为另一个用户获取另一个工作单元。因此,如果日历是可变的并且必须根据特定用户进行更改,那么您最终将不得不在每次需要时进行设置。所以实际上根本没有缓存效果!

对于应用服务器环境中的类似服务(转换日期的服务),有(我说“是”,因为我们几年前曾经这样做过)无状态和有状态 bean,现在我认为缓存服务可以更合适,每个时区填充多个实例。

只要实例是可变的,它们就必须在缓存中被锁定(可能会被删除并重新插入)。并且必须管理缓存、适当调整大小等。

最好始终保持无国籍状态 - 就像 Joda 一样。并预填充缓存,以避免在每条记录上动态创建新对象(取决于规模,另一种降低应用服务器速度的方法)。

【讨论】:

  • 感谢您的见解。这个意见很有价值。
【解决方案3】:

这个解决方案怎么样?:

目前,JulianDay 是一个只有静态成员的类(所有方法都是静态的),我没有创建 JulianDay 的实例。

我建议像这样重构 JulianDay:

  • 将 JulianDay 改造成一个可实例化的类
  • 将 JulianDay 实例设为 ThreadLocal
  • 使 JulianDay 实例不可变
  • 对 JulianDay 构造函数使用单个字符串参数(日历的时区 ID)
  • 让 JulianDay 封装并限制 GregorianCalendar
  • 在内部同步对 GregorianCalendar 的访问,以便原子使用

我相信如果按照上述方式开发 JulianDay 实例将是线程安全的,最好的部分是我不需要对现有代码进行大量重构即可使其与可实例化的 JulianDay 实例一起使用。

关键部分是不让 GregorianCalendar 逃跑。

【讨论】:

  • 我猜如果 JulianDay 的实例是不可变的,那么就没有必要在 ThreadLocals 中使用它们。如果我在内部同步使用 GregorianCalendar,我应该能够像使用任何其他不可变对象一样使用 JulianDays?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-07-04
  • 2014-09-19
  • 1970-01-01
  • 2012-12-19
  • 2010-09-21
  • 2012-07-06
  • 1970-01-01
相关资源
最近更新 更多