Bil*_*Ape 4 java concurrency multithreading synchronized
给定一个理论系统,如果在本地系统中找不到文件,则从 Web 下载文件并假设:
我编写了一个方法,使用 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?为什么?
似乎正在进行货物崇拜编程。第三种变体类似于双重检查锁定,但没有\xe2\x80\x99 明白这一点。但更引人注目的是,第一个变体也类似于双重检查锁定,getFileFromLocalFS(url)无缘无故地调用两次。而\xe2\x80\x99就是起点\xe2\x80\xa6
您对面试官\xe2\x80\x99s \xe2\x80\x9cimprovement\xe2\x80\x9d 的怀疑是正确的。嵌套同步没有任何作用,在最好的情况下,JVM\xe2\x80\x99s 优化器将能够消除它。
\n然而,这两种解决方案都不推荐。这里有一个更深层次的问题。首先,在讨论性能之前,我们应该关注正确性。
\n显然,getFileFromLocalFS(url)应该找到本地副本(如果存在),但不创建它。如果没有人创建这样的副本,并且如果此方法不执行缓存尝试,那么关于重叠缓存尝试的整个问题将毫无意义,这将毫无意义。因此,涉及的唯一其他方法getFileFromWeb(url)必须是创建本地副本的方法。
这意味着这两种方法之间存在隐藏协议。getFileFromLocalFS(url)不知何故知道如何获取getFileFromWeb(url).
这就提出了一些问题,例如,如果一个线程正在创建getFileFromWeb(url)缓存版本,而另一个线程调用getFileFromLocalFS(url)相同的 url,会发生什么情况?它是否将输入流返回到仍然不完整的本地文件?有两种可能:
这个问题已经以某种方式解决了。这意味着幕后已经有一些线程安全的构造来防止这种竞争条件,因此,它应该首先用来getFileFromWeb(url)解决多次缓存尝试的问题。
或者这个问题没有得到解决,并且起点是一个损坏的方法,所以第一个任务应该是修复这个问题,而不是试图提高性能。如果我们真的尝试在两个方法之外(即在 中)修复此问题,则getFile只有第二个变体是正确的,即在 中调用这两个方法synchronized。
无论哪种情况,该getFile方法都不是解决问题的地方。并发问题应该通过两种方法之间已经存在的隐藏协议来解决。
例如:
\n如果这两种方法使用从 url 到本地文件的映射,则应使用并发映射及其原子更新操作来解决并发问题。
\n如果这两个方法使用映射方案将 url 转换为副本所在的本地路径,并且该getFileFromLocalFS(url)方法仅检查文件是否存在,则该方法应该创建带有自动检查是否存在的选项的getFileFromWeb(url)文件CREATE_NEW如果不存在则创建,并与文件锁定一起防止其他线程在缓存仍在进行时读取。