对局部变量进行同步

时间:2011-05-25 10:29:51

标签: java multithreading

今天我遇到了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));
            }
        }
    }
}

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

5 个答案:

答案 0 :(得分:71)

这是手动“lock coarsening”的示例,可能是为了提升性能。

考虑这两个片段:

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 Hotspot Server Compiler没有机会优化代码,例如通过消除转义对象的锁。

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)WITH WARM-UP:

在此测试中,方法在计算时间之前运行了几次。这样做是为了让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);  
    }
}

答案 1 :(得分:7)

缺乏经验,无能力,或者更有可能是在重构后仍然存在的良性代码。

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

(从更广泛的意义上说,有时对于本地变量同步很有用 - 毕竟它们仍然是对象,而另一个线程仍然可以引用它们(只要它们有在他们创作之后以某种方式“发布”了。但是,这很少是一个好主意,因为它往往不清楚并且很难做到正确 - 在这些情况下,与其他线程更明确的锁定机制可能总体上证明更好。)

答案 2 :(得分:5)

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

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

答案 3 :(得分:3)

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

答案 4 :(得分:1)

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