【问题标题】:Is it safe in Java to read (not modify) objects which are not thread safe (like linked list) from multiple threads?在 Java 中从多个线程读取(而不是修改)不是线程安全的对象(如链表)是否安全?
【发布时间】:2015-03-12 17:03:01
【问题描述】:

已经有一个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();
}

【问题讨论】:

    标签: java multithreading synchronization


    【解决方案1】:

    是的,在您的特定示例代码中没关系,因为创建新线程的行为应该定义填充列表和从另一个线程读取它之间的发生前关系。但是,设置可能不安全。

    我强烈推荐阅读 Brian Goetz 等人的“Java Concurrency in Practice”以了解更多详细信息。

    【讨论】:

    【解决方案2】:

    虽然一个答案是警告“未刷新缓存”并建议了解“Java 内存模型”。

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

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

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

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

    Java 内存模型……很难。

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

    更重要的是,Goetz 等人为您提供了一组更简单的规则,这些规则足以为您提供线程安全。这些规则仍然过于详细,无法浓缩成 StackOverflow 答案……但是

    • 其中一个概念是“安全发布”,并且
    • 其中一个原则是使用/重用现有的并发构造,而不是基于内存模型滚动您自己的并发机制。

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

    它>>是

    (至少从我的测试中)

    ... 测试并不能保证任何事情。非线程安全程序的问题在于,错误经常无法通过测试揭示出来,因为它们是随机出现的,概率很低,而且在不同平台上往往不同。

    您不能依靠测试来告诉您您的代码是线程安全的。您需要对行为进行推理1...或遵循一套有根据的规则。


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

    【讨论】:

    • 感谢您的回复。虽然大多数情况下我只知道这很困难 :) 感谢推荐这本书,我会将它添加到我的待办事项阅读列表中。也感谢您从 Java 内存模型和缓存视图中批准我的解决方案......我很高兴到目前为止我直观地得到了它......
    • 是的……但是一点点改变就会变得不安全。除非您真正了解 Java 内存模型,否则您的直觉并不可靠。
    【解决方案3】:

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

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

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

    【讨论】:

      【解决方案4】:

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

      程序很少那么简单:

      • 如果列表包含对其他(可变)数据的引用,那么您将获得竞争条件。
      • 如果有人在代码生命周期的后期修改了您的“阅读器”线程,那么您将面临竞争。

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

      我建议像这样包装List&lt;&gt; 实例,这样如果有人尝试使用列表中的任何突变器,代码就会立即失败:

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

      链接到unmodifiableList

      【讨论】:

      • 他使用的数据结构不是一成不变的。并且为消费者创建不可变视图不会阻止任何保留对基础集合的引用的人进行修改。
      • 公平点;编辑以反映。根据他的用例,提供不可修改的视图可能是合适的。
      【解决方案5】:

      您需要保证 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 代替 LinkedList。

      【讨论】:

        【解决方案6】:

        很抱歉回答我的问题。但我在考虑你令人放心的答案,我发现它可能不像看起来那么安全。我发现并测试了它不工作的情况 - 如果对象将使用它的类变量来存储任何数据(我不知道),那么它将失败(那么唯一的问题是链表(和其他 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
            }
        }
        

        【讨论】:

        • 这根本不是线程安全问题。这(失败的例子)只是一个错误。
        • 我不明白为什么这是一个错误?我同意这么短的代码是愚蠢的——它是为了演示目的。但是想象有一些有用的东西——比如想象它会在内部存储最后访问的索引+值以加快响应速度。在这种情况下,您不应该称其为错误,我也不会称其为愚蠢的编写代码。它也适用于单线程或包装成“同步”时。
        • 好吧,也许那是夸大了。正如目前所写的那样,只有在有多个线程时,该错误才能显现出来。但问题是您使用字段作为局部变量,这使得代码不可重入。在类似的情况下,这样做会破坏单线程应用程序;例如如果myDummyGet 是递归的。此外,这不符合您问题的标准。对lastGetIndex 的赋值是对对象状态的修改。如果没有任何修改,您的问题是关于线程安全的。
        • 是的,我同意 - 不能递归。但关键是我不知道库函数的实现。有没有办法确保调用像 get() 这样看似只读的函数不会修改里面的东西??? (比如记住上次访问的值)。有没有像@NotModifyingObject 这样的注释?如果没有这样的事情,我认为它是相关的并且在我的问题范围内。每次我们使用看似只读的函数时,我们都必须使用一些 lock()/semaphore()/synchronized(),因为我们无法确定实现。对吗?
        • 没错。除非库方法或类被记录为是线程安全的(或者您已经对其进行了分析),否则您应该假设它不是并同步。
        【解决方案7】:

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

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

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

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

        可能还有其他人。或者,您可以将共享对象的所有字段设为final(包括其Object 成员的所有字段,等等)。然后通过任何方式跨线程共享这个对象都是安全的。这是不可变类型被低估的优点之一。

        【讨论】:

        • 他要求提供特定的用例。他的用例很好。在他的情况下,您提出的所有建议都不是必需的。没有必要保护代码免受与其无关的事件。
        • @Zielu 你看了题目的标题了吗?它根本不具体。阅读整个问题。他希望在“缓存”面前“详细说明”数据的可见性。
        • 我读了,他问他相信下面的代码是可以的。他询问了仅使用读取操作而不是将它们与写入混合的访问。
        • 问题本身,以及问题相对于它所指的先前答案的上下文,清楚地表明他的问题更笼统。他在哪里要求特定的用例?他提供了一个一般原则的例子。
        • 很抱歉与更有经验的用户争论,但您仍可能想编辑您的答案。您错误地建议使列表变量最终/易失(我不知道您的项目符号的语义是否是或/和)提供了线程安全性。
        【解决方案8】:

        如果您只能通过“读取”方法(包括迭代)访问列表,那么您就可以了。就像在你的代码中一样。

        【讨论】:

        • 它的意义远不止于此。只读访问不能保证“工作”。阅读“安全发布”。
        • 请你证明我错了。老实说,我不明白如何读取变量不是线程安全的。
        • 安全出版物与此无关。访问发生在创建和写入操作之后。
        • 相关。他(意外)通过在修改对象后启动线程来安全发布是使他的代码工作的原因。如果这就是你在为你的答案辩护时所指的,你应该在你的答案中提到它。
        • 我不相信引用“似乎答案是肯定的,因为没有人从链接列表中对其进行结构性更改(添加/删除)。”,“意外”写入在尝试从中读取之前列出。但我不需要再为自己的答案辩护了,我真的可以忍受这种名誉的损失。
        猜你喜欢
        • 1970-01-01
        • 2011-11-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-08-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多