【问题标题】:Creating one non-thread-safe object per thread and using happens-before guarantee为每个线程创建一个非线程安全对象并使用happens-before保证
【发布时间】:2016-07-09 04:41:06
【问题描述】:

我想将 SAAJ 中的 SOAPConnectionFactory 和 MessageFactory 类用于多线程,但事实证明我不能假设它们是线程安全的。 一些相关帖子:

这是一个有趣的小证明,证明它可以是线程安全的: http://svn.apache.org/repos/asf/axis/axis2/java/core/tags/v1.5.6/modules/saaj/src/org/apache/axis2/saaj/SOAPConnectionImpl.java 据说

虽然 SAAJ 规范没有明确要求线程安全,但 Sun 参考实现中的 SOAPConnection 似乎是线程安全的。

但我仍然认为将 SAAJ 类视为线程安全的证据还不够。

所以我的问题是:下面的成语正确吗?我使用主线程内可能的非线程安全工厂创建了一个 SOAPConnection 和 MessageFactory 对象,然后使用 CompletionService 接口的happens-before保证将这些对象安全地发布到执行程序任务。我也使用这种发生前的保证来提取结果 HashMap 对象。

基本上我只是想验证我的推理是否合理。

public static void main(String args[]) throws Exception {
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    CompletionService<Map<String, String>> completionService = new ExecutorCompletionService<>(executorService);

    //submitting 100 tasks
    for (int i = 0; i < 100; i++) {
        // there is no docs on if these classes are thread-safe or not, so creating them before submitting to the
        // external thread. This seems to be safe, because we are relying on the happens-before guarantees of the
        // CompletionService.
        SOAPConnectionFactory soapConnectionFactory = SOAPConnectionFactory.newInstance();
        SOAPConnection soapConnection = soapConnectionFactory.createConnection();
        MessageFactory messageFactory = MessageFactory.newInstance();
        int number = i;// we can't just use i, because it's not effectively final within the task below
        completionService.submit(() -> {
            // using messageFactory here!
            SOAPMessage request = createSOAPRequest(messageFactory, number);
            // using soapConnection here!
            SOAPMessage soapResponse = soapConnection.call(request, "example.com");
            soapConnection.close();
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            soapResponse.writeTo(outputStream);
            // HashMap is not thread-safe on its own, but we'll use the happens-before guarantee. See f.get() below.
            Map<String, String> result = new HashMap<>();
            result.put("soapResponse", new String(outputStream.toByteArray()));
            return result;

        });
    }

    // printing the responses as they arrive
    for (int i = 0; i < 100; i++) {
        Future<Map<String, String>> f = completionService.take();
        Map<String, String> result = f.get();
        System.out.println(result.get("soapResponse"));
    }

    executorService.shutdown();
}

/**
 * Thread-safe static method
 */
private static SOAPMessage createSOAPRequest(MessageFactory messageFactory, int number) throws Exception {
    SOAPMessage soapMessage = messageFactory.createMessage();
    SOAPPart soapPart = soapMessage.getSOAPPart();

    String serverURI = "example.com";

    SOAPEnvelope envelope = soapPart.getEnvelope();
    envelope.addNamespaceDeclaration("example", serverURI);

    SOAPBody soapBody = envelope.getBody();
    SOAPElement soapBodyElem = soapBody.addChildElement("number", "example");
    soapBodyElem.addTextNode(String.valueOf(number));

    soapMessage.saveChanges();

    return soapMessage;
}

【问题讨论】:

  • 现在您创建的不是每个线程的实例,而是每个任务的实例(因此每个将创建 100 个实例)。为什么不使用TreadLocal 来减少实例化并在不干扰的任务中重用它们?
  • 只是想知道:为什么要使用静态 main 进行测试;而不是单元测试?
  • @SashaSalauyou 是的,实际上是每个任务的实例。但我认为变化不大。从理论上讲,我可以在“提交”中创建一个对象并将其缓存在 ThreadLocal 中,以防另一个任务碰巧在同一个线程中运行,但我仍然必须在任务代码中至少调用 SOAPConnectionFactory.newInstance()这里假定的块不是线程安全的。如果我遗漏了什么,请告诉我。另外,我不太关心“重用”它们。对于我的情况,服务调用大约需要 1 分钟,因此创建对象的速度不是瓶颈。
  • @Jägermeister 这不是“测试”代码,它是一种简化的生产代码。
  • @Ruslan 如果您知道.newInstance() 的线程不安全,您可以通过ThreadLocal.withInitial() 中的工厂显式同步。对于 100 个实例,您不会看到差异,但在生产环境中重用会更有效。

标签: java multithreading concurrency thread-safety saaj


【解决方案1】:

我测试了你的代码,看起来你正在通过一个soapConnectionFactory 创建一个soapConnection,这非常好。 SAAJ 1.3 中的以下方法返回 MessageFactory 的新实例

public static MessageFactory newInstance(String protocol) throws SOAPException {
    return SAAJMetaFactory.getInstance().newMessageFactory(protocol);
}

在描述中没有关于线程安全的信息,但是通过查看代码,似乎这种方法主要使用堆栈变量,例如在堆栈中有一个 SOAPConnection 对象并使用它。如果soapConnection.call(request, "example.com") 被多个线程调用,尽管没有同步块,我看不到问题。

人们会期望线程会通过不同的连接发送它们的结果消息

【讨论】:

  • 它没有回答我的问题。我不是在问这些工厂是否是线程安全的。我已经对答案进行了调查,并得出结论认为没有足够的证据将它们视为线程安全的。只有明确的文档才能以“一般方式”证明这一点。有关如何处理此类缺少文档的情况,请参阅“实践中的 Java 并发”。
【解决方案2】:

我花了一个小时发现com.sun.xml.internal.messaging.saaj 的来源(用作Oracle JDK 中的默认SAAJ 实现)并发现WhateverFactory.newInstance() 返回的工厂都没有任何内部状态。所以它们绝对是线程安全的,不需要多次实例化。

这些工厂是:

例如,HttpSOAPConnectionFactory 在正文中实际上只有 3 行:

public class HttpSOAPConnectionFactory extends SOAPConnectionFactory {

    public SOAPConnection createConnection() throws SOAPException {
        return new HttpSOAPConnection();
    }
}

SOAPMessageSOAPConnection 怎么样——它们必须在一个线程中使用,尽管对它们进行的操作涉及多个调用。 (事实上,SOAPConnection#call() 也是线程安全的,因为 HttpSOAPConnection 除了 closed 变量之外不保存任何内部状态。它可以,但不应该被重用,除非你保证.close() 永远不会被调用,否则后续的.call() 将抛出。)处理完成后,SOAPConnection 应该被关闭并忘记,以及在特定请求-响应周期中使用的 SOAPMessage 实例。

总结:我相信你做的一切都是正确的,除了为每个调用创建单独的工厂。至少在上述实现中,这些工厂是完全线程安全的,因此您可以节省加载类的时间。


所有所说的都是指 Oracle JDK 附带的默认 SAAJ 实现。如果您使用的商业 Java EE 应用服务器(Websphere、JBoss 等)的实现可能是特定于供应商的,最好将您的问题提交给他们的支持。

【讨论】:

  • 无论如何,我仍然无法理解您使用 CompletionService 抽象的意图。您所做的一切都可以通过Future&lt;...&gt; f = executor.submit(() -&gt; { ... }) 轻松完成。所有这些Futures 你可以收集在列表中,然后一个接一个地调用f.get() 以获取每个任务的结果。 Future#get() 保证执行器处理完任务并返回结果后返回。
  • f.get() 在一项任务上会阻塞并阻止我在已经完成的任务上调用 f.get() 。这就是 CompletionService 的用途。它给了我他们到达时的结果。
  • @Ruslan 是的,你是对的。现在我明白了。我想你需要订购它们。
  • 答案没有回答我的问题。我不是在问这些工厂是否是线程安全的。我已经对这个问题进行了调查,并得出结论认为没有足够的证据将它们视为线程安全的。只有明确的文档才能以“一般方式”证明这一点。有关如何处理此类缺少文档的情况,请参阅“实践中的 Java 并发”。
  • 我强烈建议您阅读“Java 并发实践”中的相应章节,该章节解释了如何处理此类情况。首先,代码可以随时更改,维护者不必维护线程安全。其次,正如您所说,还有其他实现。但无论如何,这本书给出了更多的考虑。
【解决方案3】:

是的,您对 CompletionService 的推理是正确的—— .submit() 确保任务 lambda 将看到完整的对象,而 .take() 确保主线程只会看到完整的响应。

不过,一般情况下,您不需要这样做。 static 工厂方法应该始终是线程安全的,因为在没有整个 JVM 全局知识的情况下,没有方法可以确保它们不会在其他线程中使用,并且您不能真正编写在许多环境中依赖于它的代码。但是,有时您会看到一个实现可能会出现问题,如果一个线程尝试使用它而另一个线程正在配置它,但即使这样也很少见。

想象一个使用 SOAPConnectionFactory 的 servlet。不可能知道同一 JVM 中没有其他 Web 应用程序没有同时使用它,因此它必须是线程安全的。

所以,实际上,如果 MessageFactory.newInstance() 和 SOAPConnectionFactory.newInstance() 不是线程安全的,它们就是错误的。我会在多个线程中使用它们而无需担心,如果您真的担心,只需检查源代码。但他们真的很好。

另一方面,由静态工厂方法创建的对象(甚至是其他工厂)通常不是线程安全的,您不应该假设它们没有说明文件的文档。即使检查源代码也不够,因为如果接口没有被记录为线程安全的,那么以后有人可以将不安全状态添加到实现中。

【讨论】:

  • 很好的答案!我也喜欢关于静态工厂的讨论。不过,您能否提供一些证明链接来证明“非线程安全的静态工厂是错误”?我想我基本上理解你的解释,但只是想确保我没有遗漏任何东西,这确实是一种普遍接受的思维方式。
  • 证明链接?啊,不,对不起。我一直在做这种事情很长时间,所以我不验证我对互联网上随机人的推理。如果这让您感到困扰,请随意删除您的复选标记。如果您想确定,请查看源代码。
  • 感觉好像我错过了一些常见的概念或习语。这就是为什么我要求你提供一些证据。通常 Java 人指的是“Effective Java”或“Java Concurrency in Practice”。我试图至少找到“互联网上的一个随机人”解释说“非线程安全的静态工厂是一个错误”,但没有成功。所以,只是想确保它是真的。并且检查源代码是我首先试图避免的最不安全的方法。
  • 很多时候,当您通过“基本信息”时,将无法找到这样的直接证明。有时你能做的最好的事情就是找到应该知道的人编写的示例代码,并从中推断出接口的未说明属性。例如,如果您在 Google 上搜索“SOAPConnectionFactory 示例”,您会发现如果 .newInstance() 不是线程安全的,那么很多代码都是错误的。其中一些将来自 Oracle 或 Sun。随着时间的推移,您将了解这些东西的作者如何期望他们的类被使用,即使他们没有拼写出来。
猜你喜欢
  • 2020-01-05
  • 2011-09-29
  • 2014-12-10
  • 2011-01-01
  • 2013-09-23
  • 1970-01-01
  • 2012-03-26
  • 2016-05-15
  • 1970-01-01
相关资源
最近更新 更多