哪个更有效率,为什么?

3

在以下两种同步策略中,哪一种是优化的(即处理和生成的字节码),以及应该在哪种情况下使用它们之一。

public synchronized void addName(String name) 
{
       lastName = name;
       nameCount++;
       nameList.add(name);
}

或者

public void addName(String name) {
    synchronized(this) {
        lastName = name;
        nameCount++;
        nameList.add(name);
    }

}

同时,如何处理并发是什么?

  1. 使用 java.util.concurrent
  2. 使用上述低级方法
  3. 在 Eclipse PDE 环境下使用 JobUIJob API

谢谢


问题标题不清楚(什么更有效率?)。 - Guillaume
这段代码的目的是什么?它是真实的代码还是只是一个例子?例如,nameCount不就等同于nameList.size()吗?而且lastName不就是nameList中的最后一个元素吗?nameList有什么用?看起来很容易用一个简单的链表结构替换整个东西,我可以原子地CAS并消除完全不需要同步的需求。 - Jed Wesley-Smith
@Jed Wesley:这只是一个例子,旨在理解上述方法(从性能、用例和字节码生成的角度)之间的区别。 - Favonius
6个回答

5
  • 您更新的两个代码片段在语义上是相同的。然而,使用第二个片段中的同步块可以让您拥有更多控制权,因为您可以在不同的对象上同步,或者确实不同步不需要的方法部分。
  • 尽可能使用java.util.concurrent而不是使用同步原语,因为它允许您在更高的抽象级别上工作,并使用由非常熟练的人编写并进行了深入测试的代码。
  • 如果您在eclipse PDE中工作,则使用其API最好,因为它与平台的其他部分相结合。

感谢指出遗漏的部分。我已经更新了代码片段。 - Favonius
那么,“语义相同”是否意味着为它们生成的字节码将是相同的?如果我更喜欢其中一个,是否会有任何性能开销? - Favonius
@Favonius:不,这意味着它们在同步方面将具有相同的效果。我不知道在性能方面是否有任何区别。由于只有JIT编译器生成的机器代码才是真正重要的,因此拥有更多的字节码并不是非常相关的。 - Michael Borgwardt

5
根据IBM DeveloperWorks文章第1节,与同步块相比,同步方法生成的字节码更少。该文章解释了原因。
文章摘录如下:
当JVM执行同步方法时,执行线程会识别到该方法的method_info结构具有ACC_SYNCHRONIZED标志,然后自动获取对象锁、调用方法并释放锁。如果发生异常,则线程会自动释放锁。
另一方面,同步方法块则绕过了JVM对获取对象锁和异常处理的内置支持,并要求在字节码中明确编写该功能。如果您读取具有同步块的方法的字节码,您将看到超过十几个额外操作来管理此功能。清单1显示了调用生成同步方法和同步块的操作: 编辑以回应第一个评论 为了给其他SOer信用,这里有一个关于为什么使用同步块的好讨论。我相信您可以找到更多有趣的讨论,如果您搜索一下的话:) 使用同步方法而不是同步块是否有优势? 我个人没有必须使用同步块来锁定除this之外的其他对象,但这是SOers指出同步块的一个用途。

根据链接,同步块会生成更多的字节码,那么它们的用途是什么呢?您能否提供一个使用同步“块”比同步“方法”更有用的用例(除了同步粒度之外)?谢谢Kin U。 - Favonius

2
这在任何效率方面都不重要。有块的意义在于,您可以指定自己的锁定方式。您可以选择一个封装在对象内部的锁定方式,而不是使用“this”,这样就可以更好地控制谁可以获得锁(因为您可以使该锁对于对象外部不可访问)。如果你以“this”作为锁定标志(无论你是否在方法上放置了同步锁或使用块),你的程序中的任何内容都可以获取锁定对象,这会让你很难理解程序正在做什么。限制对锁的访问可以带来巨大的决策收益,拥有这种确定性比在某个字节码上削减更有益。

1

你可以移除所有锁定:

class Names {
  AtomicReference<Node> names = new AtomicReference<Node>();

  public void addName(final String name) {
    Node old = names.get();
    while (!names.compareAndSet(old, new Node(old, name))) {
      old = names.get();
    }
  }

  public String getName() {
    final Node node = names.get();
    return (node == null) ? null : node.name;
  }

  static class Node {
    final Node parent;
    final String name;

    Node(final Node parent, final String name) {
      this.parent = parent;
      this.name = name;
    }

    int count() {
      int count = 0;
      Node p = parent;
      while (p != null) {
        count++;
        p = p.parent;
      }
      return count;
    }
  }
}

这基本上是一个Treiber堆栈实现。您可以获取大小,当前名称,并且可以轻松地在内容上实现迭代器(尽管与您示例中的相反)。根据您的需求,还可以使用替代的写时复制容器。


1

我知道这可能只是一个例子,但如果你打算编写这样的代码,请再考虑一下。

在我看来,它看起来像是在重复信息,除非你发现需要对代码进行性能更改,否则不应该这样做。(你几乎永远不应该这样做)。

  • 如果你真的需要让这段代码在多个线程中运行,我会使用Collections.synchronizedList将nameList变成同步列表。
  • 最后一个名字应该是一个getter,它可以选择列表中的最后一个元素。
  • nameCount应该是列表的大小。

如果你像现在这样做,你还必须同步访问所有引用变量的地方,这将使代码变得难以阅读和维护。


0

很难说,因为这两个代码片段并不等价。

差异(调用add时缺乏同步)可能很重要,也可能不重要。从你给我们的信息来看,无法确定。


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