生产者消费者使用可重入锁不起作用

时间:2018-07-05 18:17:04

标签: java multithreading locking producer-consumer reentrantlock

我正尝试使用ReentrantLock来实现消费者生产者,如下所示:

class Producer implements Runnable {

    private List<String> data;
    private ReentrantLock lock;

    Producer(List<String> data,ReentrantLock lock)
    {
        this.data = data;
        this.lock = lock;
    }

    @Override
    public void run() {
        int counter = 0;
        synchronized (lock)
        {
            while (true)
            {

                if ( data.size() < 5)
                {
                    counter++;
                    data.add("writing:: "+counter);
                }
                else
                {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

    }
}

class Consumer implements Runnable{

    private List<String> data;
    private ReentrantLock lock;

    Consumer(List<String> data,ReentrantLock lock)
    {
        this.data = data;
        this.lock = lock;
    }

    @Override
    public void run() {
        int counter = 0;
        synchronized (lock)
        {
            while (true)
            {

                if ( data.size() > 0)
                {
                    System.out.println("reading:: "+data.get(data.size()-1));
                    data.remove(data.size()-1);
                }
                else
                {
                    System.out.println("Notifying..");
                    lock.notify();
                }
            }
        }

    }
}

public class ProducerConsumer  {

    public static void main(String[] args) {
        List<String> str = new LinkedList<>();
        ReentrantLock lock= new ReentrantLock();
        Thread t1 = new Thread(new Producer(str,lock));
        Thread t2 = new Thread(new Consumer(str,lock));
        t1.start();
        t2.start();
    }
}

因此,它只向列表写入一次,然后消费者无限期地等待。为什么会这样呢?生产者为什么不获取锁?

4 个答案:

答案 0 :(得分:2)

@Antoniossss是正确的。您没有正确使用ReentrantLock,而只能将其替换为Object。如果您想改用ReentrantLock(这是最新的),那么我会建议:

package Multithreading;


import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

class Producer implements Runnable{

  private List<String> data;
  private ReentrantLock lock;

  Producer(List<String> data,ReentrantLock lock)
  {
    this.data = data;
    this.lock = lock;
  }

  @Override
  public void run() {
    int counter = 0;
    while (true)
    {


        try {
          lock.lock();
          if ( data.size() < 5)
          {
            counter++;
            data.add("writing:: " + counter);
          }
        }finally {
          lock.unlock();
        }

      try {
        TimeUnit.SECONDS.sleep(1);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

}

class Consumer implements Runnable{

  private List<String> data;
  private ReentrantLock lock;

  Consumer(List<String> data,ReentrantLock lock)
  {
    this.data = data;
    this.lock = lock;
  }

  @Override
  public void run() {
    int counter = 0;
    while (true)
    { 
      try {
        lock.lock();
          if ( data.size() > 0) 
          {
            System.out.println("reading:: "+data.get(data.size()-1));
            data.remove(data.size()-1);
          }
       }finally {
         lock.unlock();
       }
       try 
       {
          TimeUnit.SECONDS.sleep(1);
       } catch (InterruptedException e) {
         e.printStackTrace();
       }
      }
    }
  }
}

public class ProducerConsumer  {

  public static void main(String[] args) {
    List<String> str = new LinkedList<>();
    ReentrantLock lock= new ReentrantLock();
    Thread t1 = new Thread(new Producer(str,lock));
    Thread t2 = new Thread(new Consumer(str,lock));
    t1.start();
    t2.start();
  }
}

我删除了通知调用,但是如果您真的需要一次让一个线程进入,请使用lock.notify()

答案 1 :(得分:1)

您使用lock()unlock()而不是ReentrantLock获取并释放synchronized

正如Antoniossss指出的那样,这是一个死锁,其中一个线程正在等待锁,而另一个线程永远不会放弃(而两个线程都试图在该锁上进行协调)。这是一种解决方案:

import static java.util.Objects.requireNonNull;

import java.util.LinkedList;
import java.util.List;

class Producer implements Runnable {
    private final List<String> data;

    Producer(List<String> data) {
        this.data = requireNonNull(data);
    }

    @Override
    public void run() {
        int counter = 0;
        while (true) {
            synchronized (data) {
                if (data.size() < 5) {
                    counter++;
                    data.add("writing:: " + counter);
                } else {
                    try {
                        data.wait();
                    } catch (InterruptedException e) {
                        return;
                    }
                }
            }
        }
    }
}

class Consumer implements Runnable {
    private final List<String> data;

    Consumer(List<String> data) {
        this.data = requireNonNull(data);
    }

    @Override
    public void run() {
        while (true) {
            synchronized (data) {
                if (data.size() > 0) {
                    System.out.println("reading:: " + data.get(data.size() - 1));
                    data.remove(data.size() - 1);
                }
                data.notify();
            }
        }
    }
}

public class ProducerConsumer {
    public static void main(String[] args) {
        List<String> data = new LinkedList<>();
        Thread t1 = new Thread(new Producer(data));
        Thread t2 = new Thread(new Consumer(data));
        t1.start();
        t2.start();
    }
}

我们已经取消了锁定对象,并在列表本身上进行了同步。生产者在while循环内进行同步,其效果是,一旦列表中至少有5个项目,生产者将等待,将列表中的监视器让出。

使用者也要在循环内部进行同步,这是至关重要的,因为在您的代码中,一旦获得监视器,它就永远不会放弃监视器。实际上,如果您首先启动了消费者(或者很不幸),那么根本不会产生任何东西。离开同步块或方法时,或者当在其上持有监视器wait()s的线程释放监视器时,但是notify()notifyAll()不会释放监视器。

如果有任何内容,消费者读取最后一个项目,然后立即通知生产者并释放锁。两个注意事项:

首先,尚不清楚您期望商品的订购顺序。您是否希望生产者生产5,消费者消耗5,依此类推,还是您希望5只是一个限制,以至于无法形成积压太大(这很好,这称为背压),但是消费者只要有货就急切地消费它们?此实现将执行后者。

第二,消费者在发布列表后立即尝试获取列表中的监视器。这是<忙>等待的一种形式,消费者和生产者随后竞相获得锁定,这可能是消费者经常赢得这场比赛,一旦列表为空,这将变得毫无意义。在Java 9或更高版本中,将调用onSpinWait放在同步块外部但在使用者的while循环内部可能是明智的。在早期版本中,yield可能是合适的。但是在我的测试中,任何一个代码都可以正常工作。

Antoniossss提出了另一个建议,即使用LinkedBlockingQueue,但是目前的代码始终采用最后一项,而使用队列将改变这种行为。取而代之的是,我们可以使用双端队列(双端队列),将项目放在最后并从末尾取出。看起来像这样:

import static java.util.Objects.requireNonNull;

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

class Producer implements Runnable {
    private final BlockingDeque<String> data;

    Producer(BlockingDeque<String> data) {
        this.data = requireNonNull(data);
    }

    @Override
    public void run() {
        int counter = 0;
        while (true) {
            counter++;
            try {
                data.put("writing:: " + counter);
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

class Consumer implements Runnable {
    private final BlockingDeque<String> data;

    Consumer(BlockingDeque<String> data) {
        this.data = requireNonNull(data);
    }

    @Override
    public void run() {
        while (true) {
            try {
                System.out.println("reading:: " + data.takeLast());
            } catch (InterruptedException e) {
                break;
            }
        }
    }
}

public class ProducerConsumer {
    public static void main(String[] args) {
        BlockingDeque<String> data = new LinkedBlockingDeque<>(5);
        Thread t1 = new Thread(new Producer(data));
        Thread t2 = new Thread(new Consumer(data));
        t1.start();
        t2.start();
    }
}

因为LinkedBlockingDeque是并发数据结构,所以我们不需要任何同步块或在这里等待或通知。我们可以简单地尝试从双端队列中puttakeLast开始,如果双端队列已满或为空,它将阻塞。此双端队列的容量为5,因此,如果生产者比原始生产者领先那么远,它将对生产者施加反压。

但是,没有什么可以阻止生产者以消费者可以消费的速度来生产要素,这意味着第一个要素可能必须等待任意长时间才能被消费。我不清楚这是否是您的代码的意图。您可以通过以下方式实现此目的:通过再次引入wait()notify(),使用Semaphore或其他方式,但我将其保留下来,因为目前尚不清楚你甚至想要它。

关于InterruptedException的最后一条笔记。如果有人在线程上调用interrupt(),就会发生这种情况,但是唯一保留对生产者线程和使用者线程的引用的对象是main()方法,它永远不会中断它们。因此,异常不应在此处发生,但是如果发生某种情况,我只需让生产者或消费者退出。在更复杂的情况下,如果线程处于睡眠状态或处于阻塞方法中(甚至在外部,如果它显式地对其进行检查),则可以将中断线程用作发信号的一种方式,但是我们不在此使用它。

答案 2 :(得分:1)

您只想Producer不间断地产生值,而Consumer不间断地消耗这些值并最终等待产生值,因为没有值,请在同步和锁定时跳过并使用LinkedBlockingQueue

只需简单地生成:

queue.put(value)

而在消费者中,

value=queue.take();

瞧,消费者将获取任何值,并等待值队列为空。

关于您的代码:

  1. 您根本没有使用ReentrantLock。您可以将其替换为new Object,并且输出将相同。 waitnotifyObject的方法。
  2. 您只能设法产生单一价值的原因在于您的消费者。实际上,您有这样的东西:

    synchronized(lock){ // aquire lock's monitor
    
    while(true){
    
      lock.notify();
     }
    
    } // release lock's monitor - never doing that, thread never leaves this block
    

这里的问题是您执行从不离开同步块。因此,您正在调用notify,但没有通过退出lock块来释放synchronized监视器。生产者已被“通知”,但要继续执行,必须重新获得lock的监视器-但可以这样做,因为消费者从不释放它。这里几乎是经典的僵局。

您可以想象房间里有几个人,只有一个拿着棍子的人可以说话。因此,第一个使用syncronized(stick)坚持下去。做它必须做的事情,并决定必须wait给其他人,因此他打电话给wait,然后将棒子传回。现在,第二人称可以说话,做他的工作,并决定给他棍子的那个人现在可以继续。他打电话给notify-现在他必须离开synchronized(stick)方块来传递球棒。如果他不这样做,那么第一人称无法继续进行-这就是您的情况。

答案 3 :(得分:1)

我想指出您犯的两个错误:

  1. 错误使用ReentrantLock

每个对象都可以用于内部锁,因此无需查找特定的Lock类。由于每个synchronized块都受一个方法限制,并且您不需要any enhanced means,因此ReentrantLock在这里是多余的。

  1. synchronized块的位置不正确。

输入同步块后,除非离开同步块,否则任何人都不能进入该块。显然,您永远不会因为while(true)而退出它。

我建议您删除ReentrantLock,并在data上进行同步。

@Override
public void run() {
    int counter = 0;
    while (true) {
        synchronized (data) {
            if (data.size() < 5) {
                data.add("writing:: " + ++counter);
            } else {
                try {
                    data.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        // we are out of the synchornized block here
        // to let others use data as a monitor somewhere else
    }
}