Java ArrayList上的Volatile/同步问题

3

我的程序长这样:

public class Main {
    private static ArrayList<T> list;

    public static void main(String[] args) {
        new DataListener().start();
        new DataUpdater().start();
    }

    static class DataListener extends Thread {
        @Override
        public void run() {
            while(true){
                //Reading the ArrayList and displaying the updated data
                Thread.sleep(5000);
            }
        }
    }

    static class DataUpdater extends Thread{
        @Override
        public void run() {
            //Continuously receive data and update ArrayList;
        }
    }
}

为了在两个线程中使用此ArrayList,我知道有两种选择:
  1. 将ArrayList设置为volatile。然而,我在这篇文章中读到,只有当“写入变量不取决于其当前值”时,才允许将变量设置为volatile。我认为在这种情况下它并不是(例如,当您在ArrayList上执行add操作时,此操作后的ArrayList内容取决于当前ArrayList的内容,或者不是吗?)。另外,DataUpdater必须不时从列表中删除一些元素,我还读到说无法从不同的线程编辑volatile变量。

  2. 将此ArrayList设置为同步变量。但是,我的DataUpdater将不断更新ArrayList,那么这是否会阻止DataListener读取ArrayList?

我是否误解了任何概念,或者是否有其他选项可以实现此目标?


дҪ еҸҜд»ҘдҪҝз”ЁconcurrencyеҢ…дёӯзҡ„йҳҹеҲ—д№ӢдёҖпјҢиҖҢдёҚжҳҜArrayListгҖӮ - Titus
1
你不能将ArrayList设为volatile。任何对象都不能被设为volatile。在Java中,唯一可以被设为volatile的是_fields_。 - Solomon Slow
3个回答

9
Volatile 关键字对你没有任何帮助。volatile 的含义是,线程 A 对共享变量所做的更改会立即对线程 B 可见。通常这些更改可能在某些仅对进行更改的线程可见的高速缓存中,而 volatile 只是告诉 JVM 不要对更新值延迟执行任何缓存或优化。
因此,它不是同步的一种手段,它只是确保变化的可见性。而且,这种变化是针对“变量”,而不是针对“由该变量引用的对象”的。也就是说,如果将 list 标记为 volatile,只有在你“将一个新列表分配给 list”时才会有所区别(而不是更改列表内容)!
你提出的另一种建议是将 ArrayList 设为同步变量。这里存在一个误解,变量本身无法被同步。唯一可以同步的是代码 - 整个方法或其中特定的块,你需要使用一个对象作为“同步监视器”。
监视器是对象本身(实际上,它是对象的逻辑部分),而不是变量。如果在旧值同步后将不同的对象分配给同一变量,则原有的监视器将不再可用。
但无论如何,同步的并不是对象,而是使用该对象进行同步的代码。
因此,您可以使用 list 作为监视器来同步对其执行的操作。但你不能将 list 设为同步。
假设你想使用列表作为监视器来同步你的操作,那么你应该设计它使写入线程不会一直持有锁。也就是说,它只是在单个读取-更新、插入等操作时抓取锁定,然后释放它。再接着抓取锁进行下一次操作,再释放它。如果你同步整个方法或所有更新循环,则另一个线程将永远无法读取它。
在读取线程中,你应该做以下操作:
List<T> listCopy;

synchronized (list) {
    listCopy = new ArrayList(list);
}

// Use listCopy for displaying the value rather than list

这是因为显示可能会很慢 - 它可能涉及I/O、更新GUI等。所以为了最小化锁定时间,你只需要从列表中复制值,然后释放监视器,以便更新线程可以完成其工作。
除此之外,在java.util.concurrent包中有许多类型的对象,它们被设计用于帮助像这样的情况,其中一方正在写入,另一方正在读取。查看文档 - 也许ConcurrentLinkedDeque适合你。

然而,在这种情况下,我为什么需要同步呢? - Xander
2
@Xander 因为列表上的操作不是原子性的。在另一个线程更新过程中进行的读取可能会看到列表处于不一致的状态。例如,如果你在 ArrayList 中删除一个项目,所有其他项目都会向左移动一个位置。如果此时你的读取器正在读取它,可能会因此跳过一个项目。 - RealSkeptic
没问题!谢谢你! - Xander
如果我在读取器类中创建列表的副本,那么这个问题也就避免了,对吧? - Xander
2
@Xander 不行,因为它需要在同步块中进行复制,并且写入应该在相同的锁上同步,就像我说的那样。复制本身与使用迭代器读取列表相同 - 如果没有同步,您将有可能跳过或重复项目等问题。(当发生这种情况时,您很可能会收到“ConcurrentModificationException”)。 - RealSkeptic

2

事实上,这两种解决方案都不足够。你需要同步对ArrayList的完整迭代以及对ArrayList的每个写访问:

synchronized(list) {
    for (T t : list) {
        ...
    }
}

and

synchronized(list) {
    // read/add/modify the list
}

那是另一种可能性,但它不会改变太多,因为OP只有一个读者。 - JB Nizet
同意,如果 OP 选择 ConcurrentSkipList 呢? - SMA
在JDK中没有这样的类。无论如何,在不知道确切用例的情况下,我无法提供最佳解决方案。我只知道两个线程必须同时访问一个List。 - JB Nizet

0
将ArrayList设置为volatile。
你不能将ArrayList设置为volatile。你不能将任何对象设置为volatile。在Java中,唯一可以是volatile的是字段。
在你的例子中,list不是一个ArrayList。
private static ArrayList<T> list;

listMain 类的一个 静态字段

volatile 关键字只有在一个线程 更新 字段,而另一个线程随后 访问 字段时才会起作用。

这行代码更新了 list,但没有更新 volatile field

list.add(e);

执行该行后,列表已更改,但字段仍指向同一列表对象。


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