对局部变量进行同步

art*_*mka 39 java multithreading

今天我面对constructServiceUrl()的是org.jasig.cas.client.util.CommonUtils班上的方法.我觉得他很奇怪:

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));
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

dog*_*ane 72

这是手动" 锁定粗化 "的一个示例,可能已经完成以提高性能.

考虑这两个片段:

StringBuffer b = new StringBuffer();
for(int i = 0 ; i < 100; i++){
    b.append(i);
}
Run Code Online (Sandbox Code Playgroud)

与:

StringBuffer b = new StringBuffer();
synchronized(b){
  for(int i = 0 ; i < 100; i++){
     b.append(i);
  }
}
Run Code Online (Sandbox Code Playgroud)

在第一种情况下,StringBuffer必须获取并释放锁100次(因为append是同步方法),而在第二种情况下,锁被获取并仅释放一次.这可以为您带来性能提升,这可能是作者为什么这么做的原因.在某些情况下,编译器可以为您执行此锁定粗化(但不能在循环结构周围执行,因为您可能会长时间持有锁定).

顺便说一句,编译器可以检测到一个对象没有从一个方法"转义",因此完全删除对象上的锁定(锁定省略),因为无论如何没有其他线程可以访问该对象.在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
Run Code Online (Sandbox Code Playgroud)

使用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
Run Code Online (Sandbox Code Playgroud)

再次,使用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);  
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 有趣,但我怀疑它是这个例子中的主要动机(因为更好的优化是使用`StringBuilder`代替,完全删除锁.) (4认同)
  • @finnw代码是在2007年编写的,因此作者可能没有使用Java5,例如我看不到泛型的使用. (4认同)
  • @oscarryz您的测试无效.您需要使用JDK1.4,因为这是有问题的代码(org.jasig.cas.client.util.CommonUtils)的编写版本.如果您使用JDK1.4重复测试,您将看到该版本同步块更快. (2认同)

And*_*yle 7

在重构之后仍然缺乏经验,无能,或更可能死亡但良性的代码.

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

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


fin*_*nnw 5

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

原因可能是政治 - 我一直处于类似的情况:一个"尖头发的老板"坚持我用一个setter方法克隆一个字符串,而不是只存储引用,因为害怕改变内容.他并没有否认字符串是不可改变的,但坚持克隆它"以防万一."因为它是无害的(就像这种同步)我没有争辩.