为什么异步线程不能同时修改ArrayList?

时间:2017-06-02 10:42:22

标签: java multithreading asynchronous

我正在研究线程并在这里遇到了这个片段:

我们创建并启动两个相同的java.lang.Thread并让他们连续修改ArrayList,而不做任何关于这是非线程安全的事情,因为我们只是在进行研究。

两个线程只是同一个类NoteThread的实例。在run()方法中有两个操作:

  1. add(item)到列表
  2. 列表中的
  3. remove(0)
  4. 这两个操作在1000次迭代中执行。

    public class Solution {
        public static void main(String[] args) {
            new NoteThread().start();
            new NoteThread().start();
        }
    
        public static class Note {
    
            public static final List<String> notes = new ArrayList<String>();
    
            public static void addNote(String note) {
                notes.add(0, note);
            }
    
            public static void removeNote(String threadName) {
                String note = notes.remove(0);
                if (note == null) {
                    System.out.println("Another thread has already deleted the note");
                } else if (!note.startsWith(threadName)) {
                    System.out.println("Thread [" + threadName + "] has deleted [" + note + "]");
                }
            }
        }
    
        public static class NoteThread extends Thread{
            @Override
            public void run() {
                for (int i = 0; i < 1000; i++) {
                    Note.addNote(getName() + "-Note" + i);
                    Note.removeNote(getName());
                }
            }
        }
    }
    

    有时,当列表为空时,它可以抛出IndexOutOfBoundsException: Index: 0, Size: -1并且我无法理解这是如何可能的。

    输出示例:

    Thread [Thread-1] has deleted [Thread-0-Note597]         
    Another thread has already deleted the note         
    Thread [Thread-0] has deleted [Thread-1-Note558]         
    Another thread has already deleted the note         
    Thread [Thread-1] has deleted [Thread-0-Note635]         
    Another thread has already deleted the note         
    Thread [Thread-0] has deleted [Thread-1-Note580]

    我们可以100%确定,项目的创建总是在它被删除到同一个线程之前发生,所以我假设当线程想要删除项目时不可能遇到这种情况但没有找到一个。

    更新Sergey Rybalkin已经非常清楚地解释了程序执行顺序的概念(我最初在问题中没有提及但无论如何都是这样),最重要的是,他已经回答了问题:

      

    如果Thread 1添加了某些内容,Thread 2将无法看到更改   某些情况。

    在Java中,在内部,我们修改的每个对象实际上都在每个使用它的线程中都有一个缓存副本。因为该示例没有对线程安全做任何事情,所以我们修改的数组也被缓存到每个线程。现在,既然如此,有可能:

    注意,这就是我理解的方式,我不是专家

    1. Thread 1将数组复制到其缓存中。
    2. Thread 2将数组复制到其缓存中。
    3. Thread 1将一个项添加到其缓存的数组中。
    4. Thread 2将一个项添加到其缓存数组中。
    5. Thread 1从其缓存数组中删除项目。
    6. Thread 1将其缓存的数组刷新到实际数组中。
    7. JVM传播更改并将实际数组上传到该对象的所有用户 - Thread 2。因此,第二个线程现在具有当前为空的数组的更新版本。
    8. Thread 2从其缓存数组中删除项目。
    9. 例外:列表已空:IndexOutOfBoundsException: Index: 0, Size: -1

2 个答案:

答案 0 :(得分:2)

您正在同时修改数据结构ArrayList

  1. ArrayList不是线程安全的。
  2. 你的循环迭代不是原子的。
  3. 您没有提供任何订购的东西。
  4. 如果我们命名A1 - 添加线程1,A2 - 添加线程2,R1 - 删除int thread 1,R2 - 删除线程2和以前>。 在一次迭代中,你可以得到:

    1. A1 > R1 > A2 > R2
    2. A1 > A2 > R1 > R2
    3. A1 > A2 > R2 > R1
    4. ...
    5. 我们始终只知道A1 > R1A2 > R2 调度程序也可以在线程1中执行多次迭代,并且只能在切换到线程2之后执行。

      因此,没有理由期望在两个线程中进行任何操作顺序。在这种情况下,您拥有的只是在单个线程中添加和删除的编程顺序。但是你没有关系发生的事情。请参阅JLS 17中的详情。 但最好首先有一个基本的理解。

答案 1 :(得分:0)

如果要同时使用列表,则必须选择线程安全实现,例如java.util.Vector

public static final List<String> notes = new Vector<String>();

或者,您可以使用java.util.concurrent.CopyOnWriteArrayList