Java:如何使应用程序范围的GregorianCalendar线程安全?

时间:2013-06-06 18:12:28

标签: java calendar thread-safety

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

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

因为我有很多表需要执行此转换,并且因为Calendar对象的创建成本很高,所以我只在区域设置时区更改时才创建新的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 Calendar API上的投资,并以某种方式保持我的应用程序线程安全。 Joda时间是一个选项,但是如果可能的话我想不添加那个依赖项(无论如何我使用Java API都是对时区数据库的easymode访问)。

由于日历显然具有线索敌意,我不确定如何离开这里。

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

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

3 个答案:

答案 0 :(得分:3)

您可以使用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实例进行强有力的参考。
  3. 如果您需要访问实例 accross 线程,您必须派生自己的类并同步相关方法或寻找替代实现(检查Apache公共实例)。

答案 1 :(得分:1)

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

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

在典型的应用服务器环境中,同一个线程不仅可以重复用于连续用户,而且可以在同一个会话中重用 - 控制和保留服务器资源,每个GET都是一个工作单元,单个线程可以尽快为其服务当工作单元完成后,同一个线程可以为另一个用户获得另一个工作单元。因此,如果日历是可变的并且必须根据特定用户进行更改,则最终必须在每次需要时对其进行设置。所以几乎没有缓存效果!

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

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

最好永远保持无国籍 - 就像Joda一样。并且预先填充缓存以避免在每条记录上动态创建新对象(取决于缩放app服务器的另一种方法)。

答案 2 :(得分:0)

这作为解决方案怎么样?:

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

我建议改为像这样重构JulianDay:

  • 将JulianDay重新调整为可实例化的类
  • 将JulianDay的实例设为ThreadLocal
  • 使JulianDay的实例不可变
  • 对JulianDay构造函数使用单个String参数(日历的TimeZone ID)
  • 让JulianDay封装并限制 GregorianCalendar
  • 在内部同步对GregorianCalendar的访问,以便使用原子

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

关键部分不会让GregorianCalendar逃脱。