我需要同步一个只被一个线程修改的List吗?

9

这里有一个类,它有两个线程可以访问一个List。其中一个线程定期用更新后的副本替换列表,另一个线程将列表内容绘制到屏幕上。

public class ThreadSafePainter {
    private List<String> dataList = new ArrayList<>();

    /*
     *  starts a thread to periodically update the dataList
     */
    public ThreadSafePainter() {
        Thread thread = new Thread(() -> {
            while (true) {
                // replace out-dated list with the updated data
                this.dataList = getUpdatedData();
                // wait a few seconds before updating again
                Thread.sleep(5000);
            }
        });
        thread.start();
    }

    /*
     *  called 10 times/second from a separate paint thread
     *  Q: Does access to dataList need to be synchronized?
     */
    public void onPaint(Graphics2D g) {
        Point p = new Point(20, 20);

        // iterate through the data and display it on-screen
        for (String data : dataList) {
            g.drawString(data, p.x, p.y);
            p.translate(0, 20);
        }
    }

    /*
     *  time consuming data retrieval
     */
    private List<String> getUpdatedData() {
        List<String> data = new ArrayList<>();
        // retrieve external data and populate list
        return data;
    }
}

我的问题是,我需要同步访问dataList吗?我该如何做到这一点?这样做可以吗:
public ThreadSafePainter() {
    ...
            synchronized (this) {
                this.dataList = getUpdatedData();
            }
    ...
}

public void onPaint(Graphics2D g) {
    ...
    synchronized (this) {
        for (String data : dataList)
            ...
    }
}

5
由于getUpdatedData()每次都会创建一个新的列表,您只需要进行安全发布。在这种情况下,将dataList字段声明为volatile即可。重要的是,如果在填充后存储了列表引用并且不再修改(因为下一次更新会创建新的列表),那么这将起作用,并且读者在每个处理过程中只读取一次该引用(例如for(…: dataList))。如果在一个paint期间需要多次访问列表,则必须将其存储在本地变量中。 - Holger
4
当两个或更多线程共享任何可变的状态时,必须有某种机制来处理并发。无论是低级同步、更高级的并发类、Atomic*类还是volatile字段,都取决于实际情况,但必须始终采取一些措施。 - biziclop
@StackFlowed 不是的,这只是实际实现的一个非常简化的版本,只是为了更好地理解并发。 - Will
@Holger 感谢您的见解,我没有考虑使用 volatile,这可能更适合这种情况。 - Will
1
我同意@Holger的评估。此外,这可能超出了您问题的范围,但您似乎忽略了getUpdatedData()的实现。您需要确保它也是线程安全的编写,这可能涉及同步或使用volatile进行交接。 - JimN
显示剩余5条评论
3个回答

1
任何时候当你有超过一个线程访问相同的可变状态(几乎每次都是这样,有一些例外情况,比如当你知道该状态在其他线程的生命周期内不会发生变化时),你需要采取某种行动。在这种情况下,您正在改变字段 dataList 并且您希望另一个线程对此做出反应。因此,您需要做“某事”。最常见的解决方案是使用 synchronized,您关于如何执行此操作的概述就很好。

如果您想从某些东西中挤出最大的性能(这对于 GUI 问题来说有点荒谬),或者您想展示您对并发性的深刻理解,则可以考虑适用于更有限情况的更轻量级的替代方案。在这种情况下,您仅有一个写入者,并且该写入者仅写入单个引用。对于这种情况,volatile 就足够了。在这种代码中,我个人会坚持使用 synchronized,因为当您更改代码时,它不太可能出现错误,比如您添加了另一个写入者线程之类的情况。


0

如果您不将列表同步化,则应该将列表声明为volatile。这样读取线程可以获取到最新的列表值。

这里有一个很好的解释。


-1
官方的Java文档指出,ArrayList不是同步的。因此,您需要对其进行同步。
然而,文档还指出,只有在多个线程访问同一列表时才适用。因此,在您的情况下,没有必要对其进行同步。但是,如果您想要100%确定,可以使用这个简单的调用来同步您的List:
List<data_type> list = Collections.synchronizedList(new ArrayList<data_type>());

...其中"data_type"是您想要存储的类型值。


5
这份文件有误导性。即使这个列表不需要同步,你也需要以安全的方式发布该列表的引用。 - biziclop

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接