簡體   English   中英

如何防止使用者線程兩次刪除最后一個元素?

[英]How do i prevent my consumer-threads from removing the last element twice?

問題:

  1. 嘗試刪除最后一個元素時,為什么會出現NoSuchElementException?
  2. 我該如何解決?

我有3個類(請參見下文)將整數添加/刪除到LinkedList。 一切正常,直到刪除線程到達最后一個元素。

似乎兩個線程都試圖將其刪除。 第一個成功,第二個不成功。 但是我認為!sharedList.isEmpty() -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();
            }
        }
    }

類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.util.LinkedList.element(LinkedList.java:476)處java.util.LinkedList.getFirst(LinkedList.java:126)處的線程“ Thread-2”中的java.util.NoSuchElementException。 Java:29)在Consumer.run(Consumer.java:20)

您的例外情況很容易解釋。

        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 有關示例,請參見文檔 隊列為您完成了所有阻止和同步操作,因此您的代碼不必擔心。

編輯:關於2個while循環:您不必使用2個循環,1個循環就足夠了,但是您會遇到另一個問題:在生產者填充隊列之前,消費者可能會看到隊列為空。 因此,要么必須確保隊列中有某些內容才可以使用它,要么必須以其他方式手動停止線程。 啟動生產程序后,您的thread.sleep(1000)應該相當安全,但是即使在1秒后也不能保證線程正在運行。 使用例如CountDownLatch使其真正安全。

我想知道為什么您不使用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都是Runnable,因為這是Java中線程化的首選方式。 這樣,以后就可以將任何形式的線程包裝在它們周圍以執行,而不僅僅是原始的Thread類。

  • 我使用ExecutorService而不是Thread,它可以更輕松地控制多個線程。 線程創建,調度和其他處理在內部完成,因此我要做的就是選擇最合適的ExecutorService並在完成后調用shutdownNow()

  • 還要注意,沒有必要將InterruptedException扔到void中。 如果消費者/生產者被打斷,則這是盡快優雅地停止執行的信號。 除非我需要通知其他“線程”背后的人,否則無需再次拋出該Exception(盡管也不會造成任何傷害)。

  • 我使用關鍵字final來標注以后不會更改的元素。 這是一次提示,提示編譯器可以進行一些優化,同時也有助於防止意外更改不應更改的變量。 通過不允許在線程環境中更改變量可以避免很多問題,因為線程問題幾乎總是需要同時讀取和寫入某些內容。 如果您不能寫作,這些事情就不會發生。

花一些時間在Java庫中搜索最適合您問題的類,通常可以解決很多麻煩,並且可以大大減少代碼的大小。

嘗試切換的地方

while(!sharedList.isEmpty())

synchronized(sharedList)

我認為您不需要在removeListElement()上進行同步。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM