面试题:
写一个固定容量同步容器,拥有put和get方法,以及getCount方法,能够支持两个生产者线程以及10个消费者线程的阻塞调用
Case1(使用wait和nofify实现):
package ProductorAndConsumer;
import com.sun.xml.internal.bind.v2.runtime.output.Pcdata;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
public class PC1 <T>{
//并不进行查找,所以使用链表的效率高
final private LinkedList<T> lists = new LinkedList<>();
final private int Max = 10;
private int count = 0;
public synchronized void put(T t){
while (lists.size() == Max){
try {
/*while 99%都是和wait一起用,而不是if
*
* 原因是:wait是会释放锁的,被唤醒之后如果是if,则只执行一遍循环,接下来会执行add,这时候,
* 其他的线程可能也add,会造成溢出。因为是notifyall,并且wait醒来得到this的锁可以运行时,就直接执行add啦。
* */
this.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
//如果不满10个,则可以添加
lists.add(t);
++count;
//通知消费者进行消费(现在是叫醒所有阻塞在this上的线程)
//注意:不能使用notify,只能使用notifyAll,因为使用notifyA可能叫醒的又是一个生产者,因为生产者和消费者都是阻塞在this这一个对象上。
//如果叫醒的还是生产者,就死锁啦,注意,永远要使用notifyAll,而不要使用notify
this.notifyAll();
}
public synchronized T get(){
T t = null;
//消费者在没有product的时候阻塞
while (lists.size() == 0){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果不等于0则可以进行消费
t = lists.removeFirst();
count--;
//通知生产者进行生产,注意:不能使用notify,只能使用notifyAll,道理同上
this.notifyAll();
return t;
}
public static void main(String[] args) {
//泛型实例化为String类型的对象
PC1<String> c = new PC1<>();
//启动消费者线程
for (int i = 0; i<10; i++){
//启动10个消费者线程
new Thread(()->{
//每个消费者线程消费5个
for (int j = 0; j < 5; j++) {
System.out.println(c.get());
}
}, "c"+i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//启动2个生产者线程
for (int i = 0; i < 2; i++) {
new Thread(()->{
//每个生产者线程生产25个
for (int j = 0; j < 25; j++) {
c.put(Thread.currentThread().getName()+" "+j);
}
},"p"+i).start();
}
}
}
上面代码是使用wait和nofify实现的生产者、消费者模式。先启动消费者线程,再启动生产者线程。消费者10个线程,每次消费5个,生产者2个线程,每次生产25个。主要注意的有以下几点:
注意:
- 需要使用一个容器来缓存消费者来不及消费的,这个容器使用的是LinkedList,因为容器中并不进行查找,所以使用链表的效率高
- while 99%都是和wait一起用,而不是if
原因是:在上面的判断满的操作中,wait是会释放锁的,被唤醒之后如果是if,而这时size = Max,醒来之后不会再进行判断size是否等于Max而直接进行add操作,就会造成容器的溢出。- 生产者和消费者锁定同一个信号量时,不能使用notify,只能使用notifyAll
因为如果生产者使用notify可能叫醒的又是一个生产者,因为生产者和消费者都是阻塞在this这一个对象上。这样就产生了死锁。注意,永远要使用notifyAll,而不要使用notify
Case2(使用Lock和Condition双信号量实现):
package ProductorAndConsumer;
import java.util.LinkedList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class PC2<T> {
final private LinkedList<T> lists=new LinkedList<T>();
final private int MAX=10; //最多10个元素
private int count=0;
private Lock lock=new ReentrantLock();
//producer和consumer就是声明的两个信号量(Condition)
//可以精确的通知是消费者被叫醒还是生产者被叫醒
private Condition producer=lock.newCondition();
private Condition consumer=lock.newCondition();
public void put(T t){
lock.lock();
try {
while (lists.size()==MAX){
//注意:调用的是await,不要调wait。wait是和synchronized一起使用的
//await和wait功能上是没啥区别的。只是API调用不同
//lock和await、signalAll一起使用。synchronized和wait、notifyAll一起使用
producer.await();
}
lists.add(t);
++count;
consumer.signalAll();//只通知消费者线程进行消费
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public T get(){
T t=null;
lock.lock();
try {
while (lists.size()==0){
consumer.await();
}
t=lists.removeFirst();
count--;
producer.signalAll(); //只通知生产者进行生产
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return t;
}
public static void main(String[] args) {
PC2<String> c =new PC2<>();
//启动消费者线程
for (int i = 0; i < 10; i++) {
new Thread(()->{
for (int j = 0; j < 5; j++) {
System.out.println(c.get());
}
},"c"+i).start();
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//启动生产者线程
for (int i = 0; i < 2; i++) {
new Thread(()->{
for (int j = 0; j < 25; j++) {
c.put(Thread.currentThread().getName()+" "+j);
}
},"p"+i).start();
}
}
}
这里并未使用synchronized,而是使用的ReentrantLock。然后通过ReentrantLock创建两个Condition信号量。第一种方法只能使用notifyAll,而不能使用notify使用为生产者和消费者对同一个对象进行加锁,所以只能全部通知,而不能精确的指定到底是生产者线程被唤醒还是消费者线程被唤醒。
本例中使用Lock和Condition来实现,与第一种方式对比,condition的方式可以更加精确的指定哪些线程被唤醒。producer和consumer就是声明的两个信号量(Condition),可以精确的通知是消费者被叫醒还是生产者被叫醒
注意点:
- ReentrantLock是手动锁,必须手动进行释放锁,详见这篇文章
- ReentrantLock自己创建了一个对象锁lock,lock和await、signalAll一起使用。synchronized和wait、notifyAll一起使用。await和wait功能上是没啥区别的。只是API调用不同
- condition的方式可以更加精确的指定哪些线程(生产者or消费者)被唤醒
网友评论