【问题标题】:Can inner block synchronized improve performance of a method already synchronized?内部块同步可以提高已经同步的方法的性能吗?
【发布时间】:2022-10-15 01:47:13
【问题描述】:

给定一个理论系统,如果在本地系统中找不到文件,则从 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;
    }
    

    然后需要改进这个示意图解决方案以适应同时请求从同一 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;
    }
    

    这基本上满足了请求,但存在性能问题,因为它会阻止整个方法由另一个线程运行,直到它完成。也就是说,即使可以从本地 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;
    }
    

    我说“很好,但是要使优化工作,需要删除方法同步”,即:

    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;
    }
    

    面试官不同意,坚持离开两个都synchronized 到位。

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

【问题讨论】:

  • 嵌套同步是完全没有必要的,因为您只是锁定在您已经锁定的同一台显示器上。换句话说,#3 不会改进任何东西。从方法声明中删除synchronized 是否正确取决于getFileFromLocalFS 的作用,以及它本身是否是线程安全的。换句话说,给定信息,无法说#4 是否正确。
  • #4将是最好的。您不需要同步整个方法
  • getFileFromLocalFS 是否还有其他调用者,或者此方法是否仅从 getFile 调用?
  • @Holger 出于这个面试问题的目的(我没有编造,我实际上是在真实的工作面试中被问到的),可以假设 getFileFromLocalFS 仅从 getFile 调用。

标签: java multithreading concurrency synchronized


【解决方案1】:

似乎有Cargo Cult Programming 正在进行。第三个变体类似于Double-Checked Locking,但不明白这一点。但更引人注目的是,第一个变体也类似于双重检查锁定,无缘无故地调用getFileFromLocalFS(url) 两次。这就是起点……

你对面试官“进步”的怀疑是对的。嵌套同步没有任何作用,在最好的情况下,JVM 的优化器将能够消除它。

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

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

这意味着存在一个隐藏协议这两种方法之间。 getFileFromLocalFS(url) 不知何故知道,如何获取getFileFromWeb(url) 创建的缓存文件。

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

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

  2. 或者这个问题没有解决,出发点是破碎的方法,所以首要任务应该是解决这个问题,而不是试图提高性能。如果我们真的尝试在这两种方法之外解决这个问题,即在getFile 中,只有第二种变体是正确的,在synchronized 中调用这两种方法。

    在任何一种情况下,getFile 方法都是解决问题的错误位置。并发问题应该由隐藏协议这两种方法之间已经存在。

    例如:

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

    • 如果这两种方法使用映射方案将 url 转换为副本应该在的本地路径,并且 getFileFromLocalFS(url) 方法仅检查文件是否存在,则 getFileFromWeb(url) 方法应该使用CREATE_NEW 选项以原子方式检查存在并在不存在时创建,以及文件锁定以防止其他线程在缓存仍在进行时读取。

【讨论】:

    【解决方案2】:

    它们基本上都是double-checked locking pattern 的(有些模糊的)版本。但是鉴于对getFileFromLocalFS(url) 的调用本身可能代价高昂(这是一个I/O 调用,它总是比内存中的操作更昂贵),这种形式的模式并不理想。理想情况下,双重检查锁定的实现应该尽可能“紧密”——这意味着两个锁应该尽可能靠近彼此。

    #3 中的额外同步(面试官的建议)确实会带来额外的开销,更重要的是,在不需要时可能会阻塞。 “绩效”是一个广义的术语,对不同的人和在不同的环境中可能意味着不同的事物;但额外的同步可能(可能?)影响观察到的吞吐量很大程度上取决于具体情况。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多