繁体   English   中英

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

[英]Producer Consumer using Reentrant lock not working

我正在尝试使用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();
    }
}

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

@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();
  }
}

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

您获取和释放ReentrantLocklock()unlock()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()时,监视器将被释放,但是notify()notifyAll()不会释放监视器。

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

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

其次,消费者尝试在发布列表后立即获取列表中的监视器。 这是一种忙碌的等待形式,消费者和生产者随后竞相获得锁定,这可能是消费者经常赢得这场竞赛,一旦列表为空,它将变得毫无意义。 在Java 9或更高版本中, onSpinWait的调用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,因此,如果生产者比原始生产者领先那么远,它将对生产者施加反压。

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

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

正如您只想让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但没有通过退出synchronized块来释放lock监视器。 生产者被“通知”,但是为了继续执行,它必须重新获得lock的监视器-但是可以,因为使用者从不释放它。 这里几乎是经典的僵局。

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

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

  1. ReentrantLock的错误使用。

每个对象都可以用于内部锁,因此无需查找特定的Lock类。 由于每个synchronized块都由一个方法限制,并且您不需要任何增强方法 ,因此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
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM