2

问题:

  1. 为什么在尝试删除最后一个元素时会收到 NoSuchElementException?
  2. 我该如何解决?

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

似乎两个线程都试图删除它。第一个成功了,第二个不行。但我认为同步方法/同步属性 +!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();
    }

}

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

Consumer.removeListElement(Consumer. java:29) 在 Consumer.run(Consumer.java:20)

4

3 回答 3

3

你的例外很容易解释。在

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

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

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

如果您想使用 a 使其成为线程安全的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. 有关示例,请参阅文档。队列为您完成所有阻塞和同步,因此您的代码不必担心。

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

于 2014-02-17T10:10:33.017 回答
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)which 是您使用的便利功能,并且由于用法更加清晰,因此会产生更少的拼写错误。

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

  • 我使用 ExecutorService 而不是 Thread,它可以更轻松地控制多个线程。线程的创建、调度等处理都是在内部完成的,所以我需要做的就是选择最合适的ExecutorService,完成后调用shutdownNow()

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

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

花一些时间在 Java 库中搜索最适合您的问题的类通常会解决很多麻烦并大大减少代码的大小。

于 2014-02-17T16:28:46.113 回答
0

换个地方试试

while(!sharedList.isEmpty())

synchronized(sharedList)

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

于 2014-02-17T10:11:13.897 回答