在两个线程之间共享一个 ArrayList?

Posted

技术标签:

【中文标题】在两个线程之间共享一个 ArrayList?【英文标题】:Sharing an ArrayList between two threads? 【发布时间】: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 用于线程安全而无需过度锁定的东西。 【参考方案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 CyclicBarrier CountDownLatch Locks Phaser 通常的wait/notify机制

有关更多详细信息,您必须阅读规范。让我们考虑一个使用 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://***.com/a/16529462/7083385

【讨论】:

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

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

【讨论】:

我同意将Queue 设为静态并不是最好的方法。 OP 也没有说任何关于 spring 的内容,即使如此,注入也不能解决问题,因为你仍然需要知道你在注入什么。

以上是关于在两个线程之间共享一个 ArrayList?的主要内容,如果未能解决你的问题,请参考以下文章

在两个线程之间共享deadline_timer

在两个线程之间共享 QAxObject?

如何在两个线程之间共享数据

两个线程之间的列表共享

不要同时在两个线程之间共享相同的套接字

集合ArrayList和Vector的区别?