在Java中从多个线程读取(不修改)非线程安全的对象(如链接列表)是否安全?

时间:2015-03-12 17:03:02

标签: java multithreading synchronization

已经有question whether threads can simultaneously safely read/iterate LinkeList了。似乎答案是肯定的,只要没有人在结构上改变它(添加/删除)链接列表。

虽然有一个答案警告“未刷新的缓存”并建议知道“java内存模型”。所以我要求详细说明那些“邪恶”的缓存。我是一个新手,到目前为止,我仍然天真地相信以下代码是可以的(至少从我的测试中)

public static class workerThread implements Runnable {
    LinkedList<Integer> ll_only_for_read;
    PrintWriter writer;
    public workerThread(LinkedList<Integer> ll,int id2) throws Exception {
        ll_only_for_read = ll;
        writer = new PrintWriter("file."+id2, "UTF-8");
    }
    @Override
    public void run() {
        for(Integer i : ll_only_for_read) writer.println(" ll:"+i);
        writer.close();
    }
}

public static void main(String args[]) throws Exception{
    LinkedList<Integer> ll = new LinkedList<Integer>();
    for(int i=0;i<1e3;i++) ll.add(i);
    // do I need to call something special here? (in order to say:
    // "hey LinkeList flush all your data from local cache
    // you will be now a good boy and share those data among
    // whole lot of interesting threads. Don't worry though they will only read
    // you, no thread would dare to change you"
    new Thread(new workerThread(ll,1)).start();
    new Thread(new workerThread(ll,2)).start();
}

8 个答案:

答案 0 :(得分:4)

是的,在你的具体示例代码中它没关系,因为创建新线程的行为应该定义填充列表和从另一个线程读取它之间的先发生关系。&#34;然而,有很多方法看似相似的设置可能不安全。

我强烈建议阅读&#34; Java Concurrency in Practice&#34;由Brian Goetz等人提供更多细节。

答案 1 :(得分:2)

  

虽然有一个答案警告“未刷新的缓存”并建议知道“java内存模型”。

我认为你指的是我对这个问题的回答:Can Java LinkedList be read in multiple-threads safely?

  

所以我要求详细说明那些“邪恶”的缓存。

他们不是邪恶的。它们只是生活中的事实...它们会影响多线程应用程序的正确性(线程安全性)推理。

Java内存模型是Java对这一事实的回答。内存模型以数学精度指定了一系列需要遵守的规则,以确保应用程序的所有可能执行都“格式良好”。 (简单来说:您的应用程序是线程安全的。)

Java内存模型很难。

有人推荐Brian Goetz等人的“Java Concurrency in Practice”。我同意。它是编写“经典”Java多线程应用程序主题的最佳教科书,它对Java内存模型有很好的解释。

更重要的是,Goetz等人为您提供了更简单的规则,这些规则足够为您提供线程安全性。这些规则仍然过于详细,不能压缩成StackOverflow的答案......但

  • 其中一个概念是“安全出版物”和
  • 其中一个原则是使用/重用现有的并发结构,而不是根据内存模型推出自己的并发机制。
  

我是新手,到目前为止,我仍然天真地相信以下代码是可以的。

它&gt;&gt;是&lt;&lt;正确。但是......

  

(至少从我的测试中)

...测试不是任何保证。非线程安全程序的问题在于,故障通常不会通过测试显示,因为它们以低概率随机显示,并且在不同平台上通常不同。

您不能依赖测试来告诉您代码是线程安全的。你需要推理 1 关于这种行为......或者遵循一套有根据的规则。


1 - 我的意思是真实的,有根据的推理......不是直观的东西。

答案 2 :(得分:1)

如果您的代码创建并使用单个线程填充列表,并且只在第二时刻创建其他线程并同时访问列表,则没有问题。

只有当一个线程可以修改一个值而其他线程试图读取相同的值时才会出现问题。

如果您更改检索的对象(如果您不更改列表本身),则可能会出现问题。

答案 3 :(得分:1)

你使用它的方式很好,但只是巧合。

程序很少是微不足道的:

  • 如果List包含对其他(可变)数据的引用,那么您将获得竞争条件。
  • 如果有人修改了您的“读者”。线程在代码生命周期的后期,然后你就会参加比赛。

根据定义,不可变数据(和数据结构)是线程安全的。 然而,这是一个可变的列表,即使你与自己达成了协议,你也不会修改它。

我建议像这样包装List<>实例,这样如果有人试图在List上使用任何mutator,代码就会立即失败:

List<Integer> immutableList = Collections.unmodifiableList(ll); //...pass 'immutableList' to threads.

链接到unmodifiableList

答案 4 :(得分:0)

这取决于对象是如何创建并可供线程使用的。一般来说,不,它不安全,即使对象没有被修改。

以下是使其安全的一些方法。

首先,创建对象并执行必要的任何修改;如果不再进行修改,您可以认为该对象是有效不可变的。然后,通过以下方法之一与其他线程共享有效的不可变对象:

  • 让其他线程从volatile字段中读取对象。
  • synchronized块内写入对象的引用,然后让其他线程在同一个锁上synchronized读取该引用。
  • 初始化对象后启动读取线程,将对象作为参数传递。 (这是你在你的例子中所做的,所以你是安全的。)
  • 使用BlockingQueue实现之类的并发机制在线程之间传递对象,或者将其发布到并发集合中,如ConcurrentMap实现。

可能还有其他人。或者,您可以创建共享对象final的所有字段(包括其Object成员的所有字段,等等)。然后,通过任何方式跨线程共享此对象将是安全的。这是不可改变类型的一个不被重视的美德。

答案 5 :(得分:0)

您需要保证LinkedList中读取和写入之间的关系发生,因为它们是在不同的线程中完成的。

ll.add(i)的结果对于新的workerThread是可见的,因为Thread.start形式发生在关系之前。所以你的例子是线程安全的。详细了解happens-before conditions

但是要注意更复杂的情况,当在工作线程中迭代期间读取LinkedList并且同时由主线程修改它。像这样:

for(int i=0;i<1e3;i++) {
    ll.add(i);
    new Thread(new workerThread(ll,1)).start();
    new Thread(new workerThread(ll,2)).start();
}

这样就可以使用ConcurrentModificationException。

有几种选择:

  1. 在workerThread中克隆LinkedList并迭代副本 代替。
  2. 同时使用同步进行列表修改和列表 迭代(但会导致并发性差)。
  3. 使用CopyOnWriteArrayList
  4. 代替LinkedList

答案 6 :(得分:0)

很抱歉回答我的问题。但是我在考虑你的安慰答案,我发现它可能不像看起来那么安全。我发现并测试了它不工作时的情况 - 如果对象会使用它的类变量来存储任何数据(我不知道)那么它就会失败(那么唯一的问题就是链表(如果链接列表)和某些实现中的其他java类)可以做到......)见失败的例子:

public class DummyLinkedList {
    public LinkedList<Integer> ll;
    public DummyLinkedList(){
        ll = new LinkedList<Integer>();
    }
    int lastGetIndex;
    int myDummyGet(int idx){
        lastGetIndex = idx;
        //return ll.get(idx); // thids would work fine as parameter is on the stack so uniq for each call (at least if java supports reentrant functions)
        return ll.get(lastGetIndex); // this would make a problem even for only readin the object - question is how many such issues java.* contains
    }
}

答案 7 :(得分:-2)

如果您只通过'读'方法(包括迭代)访问列表,那么您没问题。就像你的代码一样。