如何防止我的消费者线程两次删除最后一个元素?

时间:2014-02-17 09:49:02

标签: java multithreading nosuchelementexception

问题:

  
      
  1. 尝试删除最后一个时,为什么会出现NoSuchElementException   元件?
  2.   
  3. 我该如何解决?
  4.   

我有3个类(见下文),它们将整数添加/删除到LinkedList。 一切正常,直到删除线程到达最后一个元素。

似乎两个线程都试图删除它。第一个成功,第二个成功。 但我认为synchronized-method / synchroniced-attribute + !sharedList.isEmpty()可以处理它。

班级制作人 这个类应该创建随机数,将它们放在 sharedList 中,写入控制台,它只是添加了一个数字,一旦被中断就停止。预计此类只有1个主题。

import java.util.LinkedList;

    public class Producer extends Thread
    {

        private LinkedList sharedList;
        private String name;

        public Producer(String name, LinkedList sharedList)
        {
            this.name = name;
            this.sharedList = sharedList;
        }

        public void run()
        {
            while(!this.isInterrupted())
            {
                while(sharedList.size() < 100)
                {
                    if(this.isInterrupted())
                    {
                        break;
                    } else 
                    {
                        addElementToList();
                    }
                }
            }
        }

        private synchronized void addElementToList() 
        {
            synchronized(sharedList)
            {
                sharedList.add((int)(Math.random()*100));
                System.out.println("Thread " + this.name + ": " + sharedList.getLast() + " added");
            }
            try {
                sleep(300);
            } catch (InterruptedException e) {
                this.interrupt();
            }
        }
    }

Class Consumer:此类应该删除sharedList中的第一个元素(如果存在)。执行应该继续(在被中断之后),直到 sharedList 为空。预期此类的多个(至少2个)线程。

import java.util.LinkedList;

public class Consumer extends Thread
{
    private String name;
    private LinkedList sharedList;

    public Consumer(String name, LinkedList sharedList)
    {
        this.name = name;
        this.sharedList = sharedList;
    }

    public void run()
    {
        while(!this.isInterrupted())
        {
            while(!sharedList.isEmpty())
            {
                removeListElement();
            }
        }
    }

    private synchronized void removeListElement()
    {
        synchronized(sharedList)
        {
            int removedItem = (Integer) (sharedList.element());
            sharedList.remove();
            System.out.println("Thread " + this.name + ": " + removedItem + " removed");
        }
        try {
            sleep(1000);
        } catch (InterruptedException e) {
            this.interrupt();
        }
    }
}

类MainMethod:此类应该启动并中断线程。

import java.util.LinkedList;


public class MainMethod 
{

    public static void main(String[] args) throws InterruptedException 
    {
        LinkedList sharedList = new LinkedList();
        Producer producer = new Producer("producer", sharedList);
        producer.start();
        Thread.sleep(1000);
        Consumer consumer1 = new Consumer("consumer1", sharedList);
        Consumer consumer2 = new Consumer("consumer2", sharedList);
        consumer1.start();
        consumer2.start();
        Thread.sleep(10000);
        producer.interrupt();
        consumer1.interrupt();
        consumer2.interrupt();
    }

}

例外:这是我得到的确切例外。

  

线程“Thread-2”中的异常java.util.NoSuchElementException at   java.util.LinkedList.getFirst(LinkedList.java:126)at   java.util.LinkedList.element(LinkedList.java:476)at   Consumer.removeListElement(Consumer.java:29)at   Consumer.run(Consumer.java:20)

3 个答案:

答案 0 :(得分:3)

你的例外很容易解释。在

        while(!sharedList.isEmpty())
        {
            removeListElement();
        }

sharedList.isEmpty()在同步之外发生,因此一个消费者仍然可以看到列表为空,而另一个消费者已经采用了最后一个元素。

错误地认为它是空的消费者不会试图删除不再存在导致您崩溃的元素。

如果你想使用LinkedList使其成为线程安全的,那么你必须每个读/写操作原子。例如。

while(!this.isInterrupted())
{
    if (!removeListElementIfPossible())
    {
        break;
    }
}

// method does not need to be synchronized - no thread besides this one is
// accessing it. Other threads have their "own" method. Would make a difference
// if this method was static, i.e. shared between threads.
private boolean removeListElementIfPossible()
{
    synchronized(sharedList)
    {
        // within synchronized so we can be sure that checking emptyness + removal happens atomic
        if (!sharedList.isEmpty())
        {
            int removedItem = (Integer) (sharedList.element());
            sharedList.remove();
            System.out.println("Thread " + this.name + ": " + removedItem + " removed");
        } else {
            // unable to remove an element because list was empty
            return false;
        }
    }
    try {
        sleep(1000);
    } catch (InterruptedException e) {
        this.interrupt();
    }
    // an element was removed
    return true;
}

生产者中存在同样的问题。但他们只会创造第110个元素或类似的东西。

问题的一个好方法是使用BlockingQueue。有关示例,请参阅documentation。队列完成所有阻塞和为您同步所以您的代码不必担心。

编辑:关于2 while循环:你不必使用2个循环,1个循环循环足够但你会遇到另一个问题:消费者可能会在生产者填充它之前看到队列为空。因此,您必须确保队列中有某些内容才能被消耗,或者您必须以其他方式手动停止线程。启动生产者后你thread.sleep(1000)应该是相当安全的,但是即使1秒后线程也不能保证运行。使用例如CountDownLatch使其真正安全。

答案 1 :(得分:1)

我想知道你为什么不使用Java提供的现有类。我用这些重写了你的程序,它变得更短更容易阅读。除此之外,缺少synchronized,它阻止除了获得锁定的所有线程(你甚至进行双重同步),允许程序实际并行运行。

以下是代码:

制片:

public class Producer implements Runnable {

  protected final String name;
  protected final LinkedBlockingQueue<Integer> sharedList;
  protected final Random random = new Random();

  public Producer(final String name, final LinkedBlockingQueue<Integer> sharedList) {
    this.name = name;
    this.sharedList = sharedList;
  }

  public void run() {
    try {
      while (Thread.interrupted() == false) {
        final int number = random.nextInt(100);
        sharedList.put(number);
        System.out.println("Thread " + this.name + ": " + number);
        Thread.sleep(100);
      }
    } catch (InterruptedException e) {
    }
  }
}

消费者:

public class Consumer implements Runnable {

  protected final String name;
  protected final LinkedBlockingQueue<Integer> sharedList;

  public Consumer(final String name, final LinkedBlockingQueue<Integer> sharedList) {
    this.name = name;
    this.sharedList = sharedList;
  }

  public void run() {
    try {
      while (Thread.interrupted() == false) {
        final int number = sharedList.take();
        System.out.println("Thread " + name + ": " + number + " taken.");
        Thread.sleep(100);
      }
    } catch (InterruptedException e) {
    }
  }
}

主:

public static void main(String[] args) throws InterruptedException {
  final LinkedBlockingQueue<Integer> sharedList = new LinkedBlockingQueue<>(100);
  final ExecutorService executor = Executors.newFixedThreadPool(4);

  executor.execute(new Producer("producer", sharedList));
  Thread.sleep(1000);

  executor.execute(new Consumer("consumer1", sharedList));
  executor.execute(new Consumer("consumer2", sharedList));

  Thread.sleep(1000);
  executor.shutdownNow();
}

有几点不同:

  • 由于我使用的是并发列表,因此我不必关心(很多)同步,列表会在内部进行。

  • 由于此列表使用原子锁而不是通过synchronized进行真正的阻塞,因此使用的线程越多,它的扩展性就越好。

  • 我确实将阻塞队列的限制设置为100,所以即使生产者没有检查,列表中也不会有超过100个元素,因为put将阻止达到了极限。

  • 我使用random.nextInt(100)这是一个方便的功能,因为使用更加清晰,所以会产生更少的拼写错误。

  • Producer和Consumer都是Runnables,因为这是在Java中进行线程化的首选方式。这允许稍后在它们周围包装任何形式的线程以执行,而不仅仅是原始的Thread类。

  • 我使用的ExecutorService代替Thread,可以更轻松地控制多个线程。线程创建,调度和其他处理在内部完成,因此我需要做的就是选择最合适的ExecutorService并在完成后调用shutdownNow()

  • 另请注意,无需将InterruptedException抛出到void中。如果消费者/生产者被中断,那么这是一个尽快停止执行的信号。除非我需要通知那个线程“后面”,否则不需要再次抛出异常(尽管也没有造成任何伤害)。

  • 我使用关键字final来记录稍后不会更改的元素。一旦这是允许进行一些优化的编译器的提示,它也可以帮助我防止意外更改不应该更改的变量。通过不允许变量在线程环境中进行更改可以防止许多问题,因为线程问题几乎总是需要同时读取和写入某些内容。如果你不能写,这样的事情就不会发生。

花一些时间在Java库中搜索适合您问题的类,最好的解决方案通常可以解决很多问题并大大减少代码的大小。

答案 2 :(得分:0)

尝试切换

的位置
while(!sharedList.isEmpty())

synchronized(sharedList)

我认为你不需要在removeListElement()上进行同步。