【问题标题】:Synchronization on the local variables局部变量同步
【发布时间】:2011-05-25 10:29:51
【问题描述】:

今天我遇到了org.jasig.cas.client.util.CommonUtils类的方法constructServiceUrl()。我觉得他很奇怪:

final StringBuffer buffer = new StringBuffer();

synchronized (buffer)
{
    if (!serverName.startsWith("https://") && !serverName.startsWith("http://"))
    {
        buffer.append(request.isSecure() ? "https://" : "http://");
    }

    buffer.append(serverName);
    buffer.append(request.getRequestURI());

    if (CommonUtils.isNotBlank(request.getQueryString()))
    {
        final int location = request.getQueryString().indexOf(
                artifactParameterName + "=");

        if (location == 0)
        {
            final String returnValue = encode ? response.encodeURL(buffer.toString()) : buffer.toString();

            if (LOG.isDebugEnabled())
            {
                LOG.debug("serviceUrl generated: " + returnValue);
            }

            return returnValue;
        }

        buffer.append("?");

        if (location == -1)
        {
            buffer.append(request.getQueryString());
        }
        else if (location > 0)
        {
            final int actualLocation = request.getQueryString()
                    .indexOf("&" + artifactParameterName + "=");

            if (actualLocation == -1)
            {
                buffer.append(request.getQueryString());
            }
            else if (actualLocation > 0)
            {
                buffer.append(request.getQueryString().substring(0, actualLocation));
            }
        }
    }
}

作者为什么要同步一个局部变量?

【问题讨论】:

  • 无论正在同步什么,同步是否会产生其他副作用?
  • @TREE,没有。只要没有其他线程可以看到正在同步的对象,它就不需要任何效果。
  • 对这个问题的接受回答澄清了我错的原因:stackoverflow.com/questions/1850270/…

标签: java multithreading


【解决方案1】:

这是手动“lock coarsening”的示例,可能已经完成以提高性能。

考虑这两个 sn-ps:

StringBuffer b = new StringBuffer();
for(int i = 0 ; i < 100; i++){
    b.append(i);
}

对比:

StringBuffer b = new StringBuffer();
synchronized(b){
  for(int i = 0 ; i < 100; i++){
     b.append(i);
  }
}

在第一种情况下,StringBuffer 必须获取和释放锁 100 次(因为append 是同步方法),而在第二种情况下,锁只被获取和释放一次。这可以提高性能,这可能是作者这样做的原因。在某些情况下,编译器可以为您执行此lock coarsening(但不能围绕循环构造执行,因为您最终可能会长时间持有锁)。

顺便说一句,编译器可以检测到一个对象没有从方法中“转义”,因此完全删除了对该对象的获取和释放锁(锁省略),因为无论如何其他线程都无法访问该对象。 JDK7 已经在这方面做了很多工作。


更新:

我进行了两个快速测试:

1) 没有热身:

在这个测试中,我没有多次运行这些方法来“预热”JVM。这意味着 Java 热点服务器编译器没有机会优化代码,例如通过消除逃逸对象的锁。

JDK                1.4.2_19    1.5.0_21    1.6.0_21    1.7.0_06
WITH-SYNC (ms)         3172        1108        3822        2786
WITHOUT-SYNC (ms)      3660         801         509         763
STRINGBUILDER (ms)      N/A         450         434         475

使用 JDK 1.4,带有外部同步块的代码更快。但是,使用 JDK 5 及更高版本的代码没有外部同步会胜出。

2) 热身:

在这个测试中,这些方法在计算时间之前运行了几次。这样做是为了让 JVM 可以通过执行逃逸分析来优化代码。

JDK                1.4.2_19    1.5.0_21    1.6.0_21    1.7.0_06
WITH-SYNC (ms)         3190         614         565         587
WITHOUT-SYNC (ms)      3593         779         563         610
STRINGBUILDER (ms)      N/A         450         434         475

再一次,使用 JDK 1.4,带有外部同步块的代码更快。但是,对于 JDK 5 及更高版本,这两种方法的性能同样出色。

这是我的测试课(欢迎改进):

public class StringBufferTest {

    public static void unsync() {
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < 9999999; i++) {
            buffer.append(i);
            buffer.delete(0, buffer.length() - 1);
        }

    }

    public static void sync() {
        StringBuffer buffer = new StringBuffer();
        synchronized (buffer) {
            for (int i = 0; i < 9999999; i++) {
                buffer.append(i);
                buffer.delete(0, buffer.length() - 1);
            }
        }
    }

    public static void sb() {
        StringBuilder buffer = new StringBuilder();
        synchronized (buffer) {
            for (int i = 0; i < 9999999; i++) {
                buffer.append(i);
                buffer.delete(0, buffer.length() - 1);
            }
        }
    }    

    public static void main(String[] args) {

        System.out.println(System.getProperty("java.version"));

        // warm up
        for(int i = 0 ; i < 10 ; i++){
            unsync();
            sync();
            sb();
        }

        long start = System.currentTimeMillis();
        unsync();
        long end = System.currentTimeMillis();
        long duration = end - start;
        System.out.println("Unsync: " + duration);

        start = System.currentTimeMillis();
        sync();
        end = System.currentTimeMillis();
        duration = end - start;
        System.out.println("sync: " + duration);

        start = System.currentTimeMillis();
        sb();
        end = System.currentTimeMillis();
        duration = end - start;
        System.out.println("sb: " + duration);  
    }
}

【讨论】:

  • 很有趣,但我怀疑它是否是本示例中的主要动机(因为更好的优化是使用 StringBuilder 代替,完全解除锁定。)
  • @finnw 代码是在 2007 年编写的,所以作者可能没有使用 Java5,例如我看不到泛型的使用。
  • -1 我创建了这个 sn-p gist.github.com/3828895,反之亦然。同步几乎需要两次
  • @oscarryz 您的测试无效。您需要使用 JDK1.4,因为这是编写有问题的代码 (org.jasig.cas.client.util.CommonUtils) 的版本。如果您使用 JDK1.4 重复测试,您将看到带有同步块更快。
  • 锁粗化是一个虚假的论点!如果 StringBuffer 真的是共享的(?),同步包装器的真正原因是使所有方法调用成为原子操作的一部分,并且 StringBuilder 会比 StringBuffer 成本更低,尤其是现在已经删除了锁省略最近的 Java 版本! StringBuffer、Hashtable 和 Vector 是严重错误,因此很少使用,因为它们很容易提供错误的安全感,同样的注意事项也适用于 Collections.synchronized* 方法。
【解决方案2】:

缺乏经验、无能,或者更可能是在重构后仍然存在的死代码。

你对这个价值的质疑是对的——现代编译器将使用转义分析来确定有问题的对象不能被另一个线程引用,因此将完全忽略(删除)同步。

(在更广泛的意义上,有时在局部变量上进行同步很有用 - 毕竟它们仍然是对象,另一个线程仍然可以引用它们(只要它们有在创建后以某种方式“发布”)。不过,这很少是一个好主意,因为它通常不清楚并且很难正确 - 在这些情况下,与其他线程的更明确的锁定机制可能会证明总体上更好。)

【讨论】:

    【解决方案3】:

    我认为同步不会产生任何影响,因为buffer 在超出范围之前从未传递给另一个方法或存储在字段中,因此其他线程可能无法访问它。

    它存在的原因可能是政治性的 - 我也遇到过类似的情况:一个“尖头发的老板”坚持要我在 setter 方法中克隆一个字符串,而不是仅仅存储参考,怕内容变了。他没有否认strings are immutable,但坚持克隆它“以防万一”。因为它是无害的(就像这个同步一样)我没有争论。

    【讨论】:

      【解决方案4】:

      这有点疯狂……除了增加开销之外,它什么也没做。更不用说对 StringBuffer 的调用已经同步了,这就是为什么在没有多个线程访问同一个实例的情况下首选 StringBuilder 的原因。

      【讨论】:

        【解决方案5】:

        IMO,不需要同步该局部变量。只有当它暴露给其他人时,例如通过将其传递给将存储它并可能在另一个线程中使用它的函数,同步将是有意义的。
        但由于情况并非如此,我认为它没有用处

        【讨论】:

          猜你喜欢
          • 2014-12-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-10-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-04-06
          相关资源
          最近更新 更多