多个Java线程中的变量同步

huangapple go评论79阅读模式
英文:

Variable synchronization in multiple Java Threads

问题

<br>
我的问题是关于如何使用块synchronized,我的类FileAdapter有一个方法write,它接收HTTP连接结果的InputStream,我用它来下载文件,每下载一千字节并写入磁盘,它都会调用DownloadReport类实例的downloaded方法,以传递已下载内容。

<br>
<br>
在另一个线程中,该线程将输出显示给用户,它调用DownloadReport类的updateProgress方法,该方法负责更新在终端上显示给用户的进度条。

<br>
<br>
问题在于,如果FileAdapter类尝试在输出线程尝试更新进度条时刚好更新已下载字节数,那么问题就会出现,因为两个方法都会编辑变量intermediateDownloaded的值,该变量仅作为辅助变量,用于保存自上次更新以来已下载的字节数,以计算下载速度。

<br>
<br>
如果我在downloadedupdateProgress方法内使用***"synchronized (this)"块,它将锁定整个DownloadReport类,输出线程只能在FileAdapter***类更新已下载字节数后才能更新进度条?

<br>
<br>
FileAdapter:

public void write(InputStream content, DownloadReport downloadReport) throws IOException {
    FileOutputStream output = new FileOutputStream(file);
    byte[] buffer = new byte[1024];
    int read;
    while ((read = content.read(buffer)) != -1) {
        output.write(buffer, 0, read);
        downloadReport.downloaded(read);
    }
}

DownloadReport:

public void downloaded(int bytes) {
    intermediateDownloaded += bytes;
    downloaded += bytes;
}

public void updateProgress() {
    long now = System.currentTimeMillis();
    double delta = UnitHelper.sizeRound(((now - lastTimeUpdate) / 1000.0), 2);
    if (delta >= 1) {
        unitAdapter.convertSpeed(intermediateDownloaded, delta);
        intermediateDownloaded = 0;
        lastTimeUpdate = now;
    }
    progressBar.updateProgress(unitAdapter.finalSize,
            unitAdapter.recalculate(downloaded), unitAdapter.unity);
}
英文:

<br>
My question is about how to use the block synchronized, my class FileAdapter has a method write that receives the InputStream of the result of an HTTP connection I’m using to download a file, for every kilobyte downloaded and written to disk, it calls the downloaded method of the DownloadReport class instance that it received, for pass on what has already been downloaded.
<br>
<br>
In another Thread, which is printing the output to the user, it calls the updateProgress method, also of the DownloadReport class, this method is responsible for updating a progress bar that is displayed to the user on the terminal.
<br>
<br>
The problem will be if the FileAdapter class tries to update the amount of bytes downloaded just when the output Thread tries to update the progress bar, as both methods edit the value of the variable intermediateDownloaded, which works only as an auxiliary variable, to hold the amount of bytes downloaded since the last update, to calculate the download speed.
<br>
<br>
If I use the "synchronized (this)" block, within the downloaded and updateProgress methods, it will block the entire class DownloadReport, and the output Thread will only be able to update the progress bar after the FileAdapter class updates the number of downloaded bytes?
<br>
<br>

FileAdapter:

public void write(InputStream content, DownloadReport downloadReport) throws IOException {

    FileOutputStream output = new FileOutputStream(file);

    byte[] buffer = new byte[1024];

    int read;

    while ((read = content.read(buffer)) != -1) {
        output.write(buffer, 0, read);
        downloadReport.downloaded(read);
    }
}

DownloadReport:

public void downloaded(int bytes) {
    intermediateDownloaded += bytes;
    downloaded += bytes;
}

public void updateProgress() {

    long now = System.currentTimeMillis();
    double delta = UnitHelper.sizeRound(((now - lastTimeUpdate) / 1000.0), 2);

    if (delta &gt;= 1) {
        unitAdapter.convertSpeed(intermediateDownloaded, delta);

        intermediateDownloaded = 0;
        lastTimeUpdate = now;
    }

    progressBar.updateProgress(unitAdapter.finalSize,
            unitAdapter.recalculate(downloaded), unitAdapter.unity);
}

答案1

得分: 1

首先,这种类型的同步在过去的十年或更长时间里已经不再被广泛采用,因为:

  • 它非常难以正确实现
  • 它在性能方面存在问题(由于等待)
  • 它无法进行测试。

使用这种方法可能会导致代码中始终存在错误,可能会引发竞态条件,而您将不会知道,也无法编写单元测试来确保您的代码不受竞态条件的影响。现代在线程之间进行通信的方法是通过使用消息队列传递不可变消息。

如果您坚持以这种方式进行操作,synchronize( this ) 是一个不好的想法,因为持有对您对象的引用的任何人也可以执行 synchronized( yourObject ),然后您就会发生死锁。对于 synchronized 方法也是如此,因为编译器在幕后使用 synchronized( this ) 来实现它们。所以,也不要那样做。总是声明一个私有对象用作锁。

此外,正如您似乎已经理解的那样,同步锁需要尽量少时间处于活动状态,以避免阻塞其他可能需要获取该锁的线程。因此,它需要封装尽可能少的指令。

在您的代码案例中,如果我理解正确,您需要执行以下操作:

private final Object lock = new Object();

public void downloaded(int bytes) {
    synchronized (lock) {
        downloaded += bytes;
    }
}

然后,在稍后的代码中,无论何时访问 downloaded,您还必须在与 lock 的同步中执行,您需要找到一些其他方法来基于仅 downloaded 计算出您的奇怪的 intermediateDownloaded 变量,以便它不需要参与同步。

或者,您可以使用 java.util.concurrent.atomic.AtomicLong downloaded 替换 long downloaded,这将允许您在不需要同步的情况下更高效地读取和更新它。

英文:

First of all, this type of synchronization has fallen out of grace in the recent decade or more, because:

  • it is extremely hard to do it right
  • it suffers performance-wise (due to waiting)
  • it is untestable.

Using this approach there may always be bugs in your code, possibly leading to race conditions, and you would not know, and there is no unit test you can write to ensure that your code is free from race conditions. The modern approach to communicating between threads is by passing immutable messages using message queues.

Now, if you insist on doing it this way, synchronize( this ) is a bad idea, because whoever holds a reference to your object can also do synchronized( yourObject ) and then you have a deadlock. The same holds true for synchronized methods, because the compiler implements them using synchronized( this ) under the hood. So, do not do that either. Always declare a private object to use as a lock.

Also, as you seem to already understand, a synchronization lock needs to be active for as little as possible, in order to avoid blocking other threads that might also need to acquire that lock. So, it needs to wrap as few instructions as possible.

In the case of your code, if I understand it correctly, you need to do the following:

private final Object lock = new Object();

public void downloaded( int bytes )
{
    synchronized( lock )
    {
        downloaded += bytes;
    }
}

then further down whenever you access downloaded you must also do it in synchronization with lock, and you need to find some other way of calculating that weird intermediateDownloaded variable of yours based on downloaded only, so that it does not need to take part in the synchronization.

Alternatively, you can replace long downloaded with java.util.concurrent.atomic.AtomicLong downloaded which will allow you to read it and update it much more performantly than if you use synchronization.

huangapple
  • 本文由 发表于 2020年3月16日 00:33:37
  • 转载请务必保留本文链接:https://go.coder-hub.com/60695138.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定