【问题标题】:Sharing an ArrayList between two threads?在两个线程之间共享一个 ArrayList?
【发布时间】:2017-03-12 13:33:20
【问题描述】:

所以我有两个线程正在运行,其中一个应该从用户那里获取信息,另一个线程应该使用用户提供的信息,如下所示:

public class UserRequest implements Runnable {

@Override
public void run() {
    // TODO Auto-generated method stub
    String request;
    Scanner input = new Scanner(System.in);
    while(true)
    {
        System.out.println("Please enter request:");
        request = input.nextLine();
        try
        {
            //do something
        }
        catch(IOException e)
        {
            e.printStackTrace();
        }
    }

}

第二个线程:

public class Poller implements Runnable {

ArrayList<String> colors = new ArrayList<String>();

public void poll()
{
    for(String color : colors)
    {
        if(color == "")
        {
            //do work
        }
        else
        {
            //do work
        }
    }
}

@Override
public void run() {

    colors.add("Violet");
    colors.add("Green");
    colors.add("Yellow");
    colors.add("Orange");

    while(true)
        poll();     
}
}

我想做的是获取用户在UserRequest 对象中输入的任何输入,然后将其推入Poller 对象中的ArrayList,以便它也可以在新值上“工作”。我看过像BlockingQueue 这样的东西,但我不希望任何一个线程等待另一个线程,因为除了数据共享之外,他们还有其他任务需要完成。我该怎么做呢?

【问题讨论】:

  • ArrayList 不是线程安全的。
  • 什么是线程安全的替代方案,我将如何在线程之间共享它?
  • 正如许多人所指出的,队列可能就是您要寻找的。您可以将 ConcurrentLinkedQueue 用于线程安全而无需过度锁定的东西。

标签: java multithreading arraylist concurrency


【解决方案1】:

由于您使用了动词“push”和“poll”,看来您正在寻找Queue 而不是List

因此,我认为您正在寻找ConcurrentLinkedQueue,记录在案的here

它允许您让您的 UserRequest 对象提供它,并让您的 Poller 对象使用它。

虽然看起来你的Poller 对象会有相当高的 CPU 消耗,因为打开的while 没有任何wait

public class Poller implements Runnable {
  Queue<String> colors = new ConcurrentLinkedQueue<String>();

  public void poll() {
    while(this.colors.isEmpty()){
      Thread.currentThread().wait();
    }

    String color = this.colors.poll();

    while(color != null) {
      if(color == "") {
        //do work

      } else {
        //do work
      }

      color = this.colors.poll();
    }
  }

  @Override
  public void run() {
    colors.offer("Violet");
    colors.offer("Green");
    colors.offer("Yellow");
    colors.offer("Orange");

    while(true) {

      this.poll();
    }
  }
}

此代码需要进行一些更改才能运行,但它包含了您需要的几乎所有内容。 它的作用非常简单:它不断轮询,直到没有剩余元素为止。 一旦发生这种情况,Poller 对象会要求当前的Thread 休眠,因为如果没有Queue 中的元素,它就没有必要运行。

public class UserRequest implements Runnable {

  @Override
  public void run() {
    String request;
    Scanner input = new Scanner(System.in);

    while(true) {
      System.out.println("Please enter request:");
      request = input.nextLine();

      try {
        //do something

      } catch(IOException e) {
        e.printStackTrace();

      } finally {
        this.notifyAll(); // Notifies all sleeping threads to wake up
      }
    }
  }

如果你注意到了,我只是在你的 UserRequest 类中添加了一个 notifyAll 调用。为什么?非常简单:notifyAll 唤醒所有 waiting Threads,这正是所有没有元素的 Pollers 正在做的事情。

一旦调用,Pollers 将唤醒,检查其颜色 Queue 是否具有元素并与它们一起使用。如果Queue 没有元素,它们将再次休眠,直到UserRequest 再次唤醒它们,依此类推。

【讨论】:

    【解决方案2】:

    有两种方法可以解决这个问题:

    1) 它使用thread safe collection,如ConccurentLinkedQueue 用于生产者-消费者、工作消耗等逻辑。如果您想使用实现List interface 的类(因此,您可以采用相同的方法平时ArrayList),一定要看CopyOnWriteArrayList那边,但是注意这个类使用阻塞同步。

    2)另一种方法是使用内置的java同步工具,例如

    有关更多详细信息,您必须阅读规范。让我们考虑一个使用 Semaphore 的例子:

    private final Semaphore semaphore = new Semaphore(2, true);
    
       public void appendToList() throws InterruptedException {
         available.acquire();
         arrayList.add(.....); //put here what u need
       }
    
       public void putItem(Object x) {
         if (someLogicHere(x)) //semaphore releases counter in this place
           available.release();
       }
    

    当然,您可以结合使用所有这些,例如你可以同时使用几个semaphores,或者使用diff工具。

    【讨论】:

      【解决方案3】:

      “但我不希望任何一个线程等待另一个线程,因为除了这种数据共享之外,它们还有其他任务需要完成。”

      没有办法做到这一点。任何适当的类线程总是会遇到这样一个问题,即您需要让一个线程等待而另一个线程执行某些操作。关键是你想最小化它。您只想使线程非常短暂且很少停止,并且仅在不这样做会导致它出现故障的情况下。您可以使用其中一种同步数据结构,也可以自己编写一点同步代码。

      唯一有问题的对象是数组列表,并且您希望任一线程上的绝对最小停顿量。因此,您可能希望根据 arraylist 本身的对象对其进行同步。因此,只需在访问 arraylist 对象的点周围编写几个小同步块。

      public class Poller implements Runnable {
      
          ArrayList<String> colors;
      
          public Poller(ArrayList<String> colors) {
              this.colors = colors;
              //pass in colors object, if modified from the scanner side it must synchronize the block around the colors object too.
          }
      
          public void doWork(String color) {
              //do work
          }
      
          public void addColor(String color) {
              synchronized (colors) {
                  colors.add(color);
              }
          }
      
          @Override
          public void run() {
              while (!Thread.interrupted())
                  if (!colors.isEmpty()) {
                      String color;
                      synchronized (colors) {
                          color = colors.remove(0);
                      }
                      doWork(color); //work done outside synch
                  }
              try {
                  Thread.sleep(100);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
      

      关键是永远不要同时删除或添加东西到列表中。您不能将列表作为一个整体进行循环,因为如果工作是在循环中完成的,则会出现问题,并且数组的大小可能会发生变化,因此您不知道它是多少位。但是,您可以为此使用 ArrayList,只需同步更改数据结构的代码块并从该同步块中取出字符串,然后对其进行处理。这样,only 停顿是一个线程正在读取或写入而另一个线程需要的短暂瞬间。两者都是非常快速的操作。

      【讨论】:

        【解决方案4】:

        如果你想访问用户从 poller 对象输入的新值,那么:

        • 由于对象存储在堆中,而不是在 Poller 类中创建 arrayList 的 新实例,您只需从 UserRequest 发送列表对象的引用。这样当您更改时添加新值到 userRequest 中的 arrayList 它将反映在 Poller 正在使用的 arrayList 中。

        例如,您可以这样做:

         public class UserRequest implements Runnable {
        
        private ArrayList<String> arrayList  = new ArrayList<String>();
        
        @Override
        public void run() {
            // TODO Auto-generated method stub
            String request;
            Scanner input = new Scanner(System.in);
            while(true)
            {
                System.out.println("Please enter request:");
                request = input.nextLine();
                try
                {
        
                 Poller poller = new Poller(arrayList);
                 Thread t = new Thread(poller);
                 t.start();
        
                }
                catch(IOException e)
                {
                    e.printStackTrace();
                }
            }
        
        }
        

        您可以像这样更改 Poller 类:

         public class Poller implements Runnable {
          private ArrayList arrayList = null;    
        
          Poller(ArrayList<String> arrayList){
             this.arrayList = arrayList; 
           }
        
        public void poll()
        {
            for(String color : arrayList)
            {
                if(color == "")
                {
                    //do work
                }
                else
                {
                    //do work
                }
            }
        }
        
        @Override
        public void run() {
        
               while(true){
                poll();   
             }    
        }
        

        但是,您应该向 arrayList 添加一个侦听器,而不是在无限循环中调用 pool,以便仅在将新值添加到 List 时调用 poll()

        您可以查看此链接以了解有关将侦听器添加到 ArrayList 的更多信息:https://stackoverflow.com/a/16529462/7083385

        【讨论】:

        • 在多线程环境中使用ArrayLists会导致ConcurrentModificationException被抛出,因为不能保证读写顺序。
        • 是的,但@Jenna 想用 ArrayList 来做
        • OP 还提供了 BlockingQueue 选项,它修复了并发性,但会阻止用户输入,直到 Poller 使用它。
        • 在循环中尝试删除arrayList的对象时,您也可以获得ConcurrentModificationException
        • 如果OP想在使用arrayList中做到这一点,我认为没有其他方法
        【解决方案5】:

        您可以使用队列。队列有自己的 poll 方法。您可以将其设为静态,但我怀疑这是最好的方法。一般来说,我使用 spring 在某种包装类中实例化队列,但看起来你并没有走那条路。

        【讨论】:

        • 我同意将Queue 设为静态并不是最好的方法。 OP 也没有说任何关于 spring 的内容,即使如此,注入也不能解决问题,因为你仍然需要知道你在注入什么。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-04-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多