内部块同步可以提高已经同步的方法的性能吗?

Bil*_*Ape 4 java concurrency multithreading synchronized

给定一个理论系统,如果在本地系统中找不到文件,则从 Web 下载文件并假设:

  1. 下载机制以及从缓存(本地文件系统)中检索/放置。
  2. 每个 URL 单线程和单个请求。

我编写了一个方法,使用 getFileFromLocalFS() 和 getFileFromWeb() 来实现简化的缓存逻辑:

public InputStream getFile(String url) { // #1
    InputStream retStream = getFileFromLocalFS(url);
    if (retStream != null) {
        return retStream;           
    }
    else {
        retStream = getFileFromLocalFS(url);
        if (retStream == null) {
            return getFileFromWeb(url);
        }
    }
    return retStream;
}
Run Code Online (Sandbox Code Playgroud)

然后需要改进这个示意性解决方案,以适应从同一 URL 下载的并发请求...并将实际的“从 Web”限制为单个下载(即所有其他请求将从本地文件系统获取它)。所以,我同步了整个方法:

public synchronized InputStream getFile(String url) { // #2
    InputStream retStream = getFileFromLocalFS(url);
    if (retStream != null) {
        return retStream;           
    }
    else {
        retStream = getFileFromLocalFS(url);
        if (retStream == null) {
            return getFileFromWeb(url);
        }
    }
    return retStream;
}
Run Code Online (Sandbox Code Playgroud)

这基本上满足了请求,但存在性能问题,因为它阻止整个方法在完成之前由另一个线程运行。也就是说,即使可以从本地 FS 获取文件,getFileFromLocalFS(url)当该方法由另一个线程运行时也无法访问该文件。

我的面试官建议的性能改进是同步getFileFromLocalFS(url)块:

public synchronized InputStream getFile(String url) { // #3
    InputStream retStream = getFileFromLocalFS(url);
    if (retStream != null) {
        return retStream;           
    }
    else {
        synchronized (this) { 
            retStream = getFileFromLocalFS(url);
            if (retStream == null) {
                return getFileFromWeb(url);
            }
        }
    }
    return retStream;
}
Run Code Online (Sandbox Code Playgroud)

我说“好吧,但是为了使优化发挥作用,需要删除方法同步”,即:

public InputStream getFile(String url) { // #4
    InputStream retStream = getFileFromLocalFS(url);
    if (retStream != null) {
        return retStream;           
    }
    else {
        synchronized (this) { 
            retStream = getFileFromLocalFS(url);
            if (retStream == null) {
                return getFileFromWeb(url);
            }
        }
    }
    return retStream;
}
Run Code Online (Sandbox Code Playgroud)

面试官不同意,并坚持保留两者 synchronized

哪一个在并发环境下表现更好?#3还是#4?为什么?

Hol*_*ger 6

似乎正在进行货物崇拜编程。第三种变体类似于双重检查锁定,但没有\xe2\x80\x99 明白这一点。但更引人注目的是,第一个变体也类似于双重检查锁定,getFileFromLocalFS(url)无缘无故地调用两次。而\xe2\x80\x99就是起点\xe2\x80\xa6

\n

您对面试官\xe2\x80\x99s \xe2\x80\x9cimprovement\xe2\x80\x9d 的怀疑是正确的。嵌套同步没有任何作用,在最好的情况下,JVM\xe2\x80\x99s 优化器将能够消除它。

\n

然而,这两种解决方案都不推荐。这里有一个更深层次的问题。首先,在讨论性能之前,我们应该关注正确性

\n

显然,getFileFromLocalFS(url)应该找到本地副本(如果存在),但不创建它。如果没有人创建这样的副本,并且如果此方法不执行缓存尝试,那么关于重叠缓存尝试的整个问题将毫无意义,这将毫无意义。因此,涉及的唯一其他方法getFileFromWeb(url)必须是创建本地副本的方法。

\n

这意味着这两种方法之间存在隐藏协议。getFileFromLocalFS(url)不知何故知道如何获取getFileFromWeb(url).

\n

这就提出了一些问题,例如,如果一个线程正在创建getFileFromWeb(url)缓存版本,而另一个线程调用getFileFromLocalFS(url)相同的 url,会发生什么情况?它是否将输入流返回到仍然不完整的本地文件?有两种可能:

\n
    \n
  1. 这个问题已经以某种方式解决了。这意味着幕后已经有一些线程安全的构造来防止这种竞争条件,因此,它应该首先用来getFileFromWeb(url)解决多次缓存尝试的问题。

    \n
  2. \n
  3. 或者这个问题没有得到解决,并且起点是一个损坏的方法,所以第一个任务应该是修复这个问题,而不是试图提高性能。如果我们真的尝试在两个方法之外(即在 中)修复此问题,则getFile只有第二个变体是正确的,即在 中调用这两个方法synchronized

    \n
  4. \n
\n

无论哪种情况,该getFile方法都不是解决问题的地方。并发问题应该通过两种方法之间已经存在的隐藏协议来解决。

\n

例如:

\n
    \n
  • 如果这两种方法使用从 url 到本地文件的映射,则应使用并发映射及其原子更新操作来解决并发问题。

    \n
  • \n
  • 如果这两个方法使用映射方案将 url 转换为副本所在的本地路径,并且该getFileFromLocalFS(url)方法仅检查文件是否存在,则该方法应该创建带有自动检查是否存在的选项的getFileFromWeb(url)文件CREATE_NEW如果不存在则创建,并与文件锁定一起防止其他线程在缓存仍在进行时读取。

    \n
  • \n
\n

  • 我发现在 SO 答案中与 Caro Cult 编程的链接很讽刺,许多 Cargo Cult 程序员都去那里解决他们的问题:-) (2认同)