我应该将Jackson的ObjectMapper声明为静态字段吗?

466

杰克逊库中的ObjectMapper似乎是线程安全的

这是否意味着我应该像这样声明我的ObjectMapper为静态字段

class Me {
    private static final ObjectMapper mapper = new ObjectMapper();
}

那么,与实例级字段不同,应该怎样做呢?

class Me {
    private final ObjectMapper mapper = new ObjectMapper();
}
6个回答

625

是的,这是安全的且推荐的。

从您提供的页面中唯一需要注意的是,一旦共享了Mapper的配置,则无法修改其配置;但是您没有更改配置,所以这很好。如果您确实需要更改配置,则可以在静态块中执行该操作,也会很好。

编辑: (2013/10)

从2.0及以上版本开始,还有一种更好的方法:使用ObjectMapper构建的ObjectWriterObjectReader对象。它们完全不可变且线程安全,这意味着不可能出现线程安全问题(如果代码尝试重新配置实例,则会出现此类问题)。


38
我有些担心当调用ObjectMapper#setDateFormat()后,ObjectMapper是否仍然是线程安全的。众所周知,SimpleDateFormat不是线程安全的(参见https://dev59.com/3Ww15IYBdhLWcg3wFHtZ),因此除非每次`writeValue()`之前都克隆例如`SerializationConfig`(我表示怀疑),否则`ObjectMapper`也不会是线程安全的。你能否消除我的担忧? - dma_k
68
DateFormat 在底层确实是被克隆的。你的怀疑很好,但是不用担心。 :) - StaxMan
3
在进行大型企业应用程序的单元/集成测试期间,我遇到了一些奇怪的行为。当将ObjectMapper作为静态final类属性时,我开始遇到PermGen问题。是否有人可以解释可能的原因?我正在使用jackson-databind版本2.4.1。 - Alejo Ceballos
2
没有mapper.with()调用(因为Jackson中的“with”意味着构建一个新实例和线程安全执行)。 但是关于配置更改:没有进行检查,因此必须保护对ObjectMapper的配置访问。 至于“copy()”:是的,它创建了一个全新的副本,可以根据相同的规则进行完全重新配置,即先完全配置它,然后使用它,这是可以的。 这涉及到一些非平凡的成本(因为复制不能使用任何缓存处理程序),但这是安全的方式。 - StaxMan
2
关于“为什么不看看是否有人尝试重新配置”的问题:这将需要额外的状态保持和同步。对于Jackson 3.x,使用构建器模式,使ObjectMapperObjectReader/ObjectWriter类似不可变可能是有意义的:但这是一个重大的API更改,因此不能在2.x中完成。 另一种防止重新配置的方法是仅公开ObjectReaderObjectWriter;只让系统的一小部分访问映射器。 - StaxMan
显示剩余17条评论

88

虽然ObjectMapper是线程安全的,但我强烈反对将其声明为静态变量,特别是在多线程应用程序中。 这不仅是因为这是一种不良实践,而且因为您正在承担死锁的巨大风险。我从自己的经验中得知。我创建了一个应用程序,其中有4个相同的线程从Web服务获取和处理JSON数据。 根据线程转储,我的应用程序经常在以下命令上停顿:

Map aPage = mapper.readValue(reader, Map.class);

此外,性能不佳。当我将静态变量替换为基于实例的变量时,停顿消失了,并且性能提高了四倍。即,处理了 240 万个 JSON 文档仅用 40 分钟 56 秒,而之前需要 2.5 小时。


23
Gary的回答完全有道理。但是为每个类实例创建一个ObjectMapper实例可能会避免锁,但以后可能会对垃圾回收造成很大的负担(想象一下你为每个类实例创建一个ObjectMapper实例)。一种中间路径的方法是,不要只在整个应用程序中保持一个(公共的)静态ObjectMapper实例,而是在每个类中声明一个(私有的)静态的ObjectMapper实例。这将减少全局锁的负载(通过按类分配负载),并且也不会创建任何新对象,因此对垃圾回收也轻量级。 - Abhidemon
21
我建议一种替代方案:在某个地方保留静态的 ObjectMapper,但只获取 ObjectReader / ObjectWriter 实例(通过帮助方法),将这些读取器/写入器对象的引用保留在其他位置(或者动态调用)。这些读取器/写入器对象不仅在重新配置方面完全线程安全,而且非常轻量级(相对于映射器实例)。因此,保留数千个引用并不会增加太多内存使用。 - StaxMan
2
那么对于ObjectReader实例的调用不是阻塞的,即在多线程应用程序中调用objectReader.readTree时,线程不会被阻塞等待另一个线程,使用jackson 2.8.x。 - Xephonia
1
@Xephonia,不,对于readXxx()的调用并不是阻塞的,可以完全并发进行;特别是对于readTree() - StaxMan
1
如果readXXX调用不是阻塞的,那么为什么静态会成为问题呢?为什么这个“单一”的实例会阻塞线程呢? - karansardana
显示剩余3条评论

11

如果您不想将其定义为静态常量变量,但又想节省一些开销并保证线程安全,可以从PR中学到一个技巧。

private static final ThreadLocal<ObjectMapper> om = new ThreadLocal<ObjectMapper>() {
    @Override
    protected ObjectMapper initialValue() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return objectMapper;
    }
};

public static ObjectMapper getObjectMapper() {
    return om.get();
}

感谢作者。


7
但是存在内存泄漏的风险,因为ObjectMapper将附加到可能属于池的线程上。 - Kenston Choi
1
@KenstonChoi 不应该是问题,就我所知。线程来了又走,线程本地变量也随之而来。根据同时存在的线程数量,您可能负担得起内存,也可能负担不起,但我没有看到“泄漏”。 - Ivan Balashov
4
@IvanBalashov,但如果线程是从线程池(例如像Tomcat这样的容器)创建/返回的,它将保留。在某些情况下可能是有意的,但我们需要注意这一点。 - Kenston Choi

3
虽然在线程安全方面,声明一个静态ObjectMapper是安全的,但是你应该知道,在Java中构建静态对象变量被认为是不好的实践。详细信息请参阅为什么静态变量被认为是邪恶的?(如有需要,可以查看我的回答

简而言之,应该避免使用静态变量,因为这使得编写简洁的单元测试变得困难。例如,使用静态final ObjectMapper时,您无法将JSON序列化替换为虚拟代码或空操作。

此外,静态final会阻止您在运行时重新配置ObjectMapper。您现在可能无法设想到原因,但如果您将自己锁定在静态final模式中,则除了拆除类加载器之外,没有其他方法可以让您重新初始化它。

在ObjectMapper的情况下,这是可以接受的,但通常来说,这是一种不好的实践,并且与使用单例模式或控制反转来管理长寿命对象相比没有任何优势。


35
虽然静态的有状态的单例通常会是一个危险信号,但在某些情况下共享一个或少量实例是有足够理由的。可以使用依赖注入来实现,但同时也值得问一下是否存在需要解决的实际或潜在问题。特别是在测试方面:仅因为某些情况下可能存在问题,并不意味着它对您的使用方式也是如此。因此,了解问题很重要,但假设“一刀切”的解决方案就不好了。 - StaxMan
4
显然地,理解任何设计决策所涉及的问题是很重要的。如果您可以在不影响使用案例的情况下完成某些操作,那么根据定义,您不会造成任何问题。但我认为使用静态实例没有任何好处,并且在代码发展或移交给其他可能不理解您的设计决策的开发人员时,它会为未来带来重大麻烦。如果您的框架支持替代方案,则没有理由不避免使用静态实例,因为它们显然没有任何优势。 - JBCP
11
我认为这个讨论变得太笼统而不太有用了。我没有问题建议怀疑静态单例的好处。但对于这个特定情况,我非常熟悉使用方法,我认为不能从一组通用准则中得出具体结论。所以我就说到这里。 - StaxMan
1
晚了点评论,但ObjectMapper不会特别反对这个观点吗?它公开了readerForwriterFor,可以根据需要创建ObjectReaderObjectWriter实例。因此,我建议将映射器与初始配置放在某个静态位置,然后根据需要获取具有每个案例配置的读取器/写入器。 - Carighan
1
@Carighan 是的,这对我来说似乎是一个很好的模式;将映射器视为用于实际使用的读取器/写入器实例的工厂。 - StaxMan

1
这个问题可能有点老了,但这是我做的事情。
ObjectMapper 实例保存在一个线程安全的单例中:
public final class JacksonObjectMapperHolder {

  private static volatile JacksonObjectMapperHolder INSTANCE;

  private static final Object MUTEX = new Object();

  public static JacksonObjectMapperHolder getInstance() {
    JacksonObjectMapperHolder instance = INSTANCE;

    if(instance == null) {
      synchronized(MUTEX) {
        instance = INSTANCE;

        if(instance == null) {
          INSTANCE = instance = new JacksonObjectMapperHolder();
        }
      }
    }

    return instance;
  }

  private final ObjectMapper objectMapper = new ObjectMapper();

  private JacksonObjectMapperHolder() {
    super();
  }

  public final ObjectMapper getObjectMapper() {
    return objectMapper;
  }

}

除此之外,如果您需要ObjectMapper而不是读取器/写入器,则私有的JacksonObjectMapperHolder()是一个很好的解决方案。 - Roie Beck
@RoieBeck 我部分同意。这个类是final的,所以隐式声明的构造函数对继承不是问题。然而,我想避免意外实例化,所以我明确地声明了构造函数并将其标记为private。调用super的繁琐表明了我避免隐式代码的选择。 - Oliver
1
只是提供我的意见,这是你的代码 :), 顺便说一下,我选择了ThreadLocal<OM>解决方案,因为它实现了相同的目标,但我认为更加优雅... - Roie Beck
这种方法相比于只使用一个静态final字段来访问ObjectMapper有什么优势? - maxeh
2
@Oliver,你说得对。如果更改objectMapper的配置,则它不是线程安全的。这也在StaxMan在此问题的被接受答案中有所解释。但是,使用你的代码,你会遇到完全相同的问题,因此30行代码和3行代码之间没有区别,因为你描述的问题与objectMapper本身的状态有关。 - maxeh
显示剩余3条评论

1

com.fasterxml.jackson.databind.type.TypeFactory._hashMapSuperInterfaceChain(HierarchicType)

com.fasterxml.jackson.databind.type.TypeFactory._findSuperInterfaceChain(Type, Class)
  com.fasterxml.jackson.databind.type.TypeFactory._findSuperTypeChain(Class, Class)
     com.fasterxml.jackson.databind.type.TypeFactory.findTypeParameters(Class, Class, TypeBindings)
        com.fasterxml.jackson.databind.type.TypeFactory.findTypeParameters(JavaType, Class)
           com.fasterxml.jackson.databind.type.TypeFactory._fromParamType(ParameterizedType, TypeBindings)
              com.fasterxml.jackson.databind.type.TypeFactory._constructType(Type, TypeBindings)
                 com.fasterxml.jackson.databind.type.TypeFactory.constructType(TypeReference)
                    com.fasterxml.jackson.databind.ObjectMapper.convertValue(Object, TypeReference)

在类com.fasterxml.jackson.databind.type.TypeFactory中,方法_hashMapSuperInterfaceChain是同步的。在高负载下看到了对同一个方法的争用。
也许是避免使用静态ObjectMapper的另一个原因。

4
请务必查看最新版本(如果可能请在此指出您使用的版本)。基于已报告的问题,锁定和类型解析(例如 Jackson 2.7中的完全重写)已经得到改进。虽然在这种情况下,“TypeReference”使用起来有点昂贵:如果可能,将其解析为“JavaType”可以避免相当多的处理(出于某些原因,无法缓存“TypeReference”,我不会在此深入讨论),因为它们是“完全解析”的(超级类型,泛型等)。 - StaxMan

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