写一个固定容量同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及10个消费者线程的阻塞调用。
1、synchronized同步方法
public class Container1<T> {
final private LinkedList<T> lists = new LinkedList<>();
final private int MAX = 10; //最多10个元素
private int count = 0;
//生产者
public synchronized void put(T t) {
while(lists.size() == MAX) { //使用while不断判断是否等于MAX值,当数量达到MAX值后使得生产线程阻塞,被消费一个便立刻结束循环唤醒生产线程
try {
this.wait(); //effective java
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lists.add(t);
++count;
this.notifyAll(); //֪ͨ通知消费者线程进行消费
}
//消费者
public synchronized T get() {
T t = null;
while(lists.size() == 0) {//一旦数量等于0,便使消费线程阻塞
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
t = lists.removeFirst();
count --;
this.notifyAll(); //通知生产者线程进行生产
return t;
}
public static void main(String[] args) {
Container1<String> c = new Container1<>();
//启动消费者线程
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 {
Thread.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();
}
}
}
程序中定义了MAX变量来限制产品的总数,定义了count变量用来判断生产了几个 产品和消费了几个 产品,在put()方法中,首先判断 LinkedList集合中产品是否是MAX变量的值,如果是启动所有消费者线程,反之开始生产 产品,在get()方法中,首先判断是否还有 产品,也就是MAX的值是否为0,如果为0通知所有生产者线程开始生产 产品,反之不为0 产品数就继续减少,需要注意的点是,这里我们加了synchronized,因为++count生产产品变成新值时,一个线程还没来得及加的时候,咋们的count还为老值,另外一个线程读到的值很可能还不是最新值,所以不加锁就会出问题,main方法中通过for循环分别创建了2个生产者线程生产分别生产25个 产品,也就是50个产品,10个消费者线程每个消费者消费5个 产品,也就是50个 产品,首先启动消费者线程,然后启动生产者线程。
在判断容器中为空或者满的时候为什么用while而不是用if? 因为当LinkedList集合中产品数等于最大值的时,if在判断了集合的大小等于MAX的时候,调用了wait()方法以后,它不会再去判断一次,方法会继续往下运行,假如在你wait()以后,另一个方法又添加了一个产品,你没有再次判断,就又添加了一次,造成数据错误,就会出问题,因此必须用while。
注意看我们用的是notifyAll()来唤醒线程的,notifyAll()方法会叫醒等待队列的所有方法,那么我们都知道,用了锁以后就只有一个线程在运行,其他线程都得wait(),不管你有多少个线程,这个时候被叫醒的线程有消费者的线程和生产者的线程,所有的线程都会争抢这把锁,比如说我们是生产者线程,生产满了,满了以后我们叫醒消费者线程,可是很不幸的是,它同样的也会叫醒另外一个生产者线程,假如这个生产者线程拿到了刚才第一个生产者释放的这把锁,它又wait()一遍,wait()完以后,又叫醒全部的线程,然后又开始争抢这把锁,其实从这个意义上来讲,生产者的线程wait时,没有必要去叫醒别的生产者。
所以基于这样的考虑,我们生产者只叫醒消费者线程,消费者线程只负责叫醒生产者线程。
2、ReentrantLock锁
public class Container2<T> {
final private LinkedList<T> lists = new LinkedList<>();
final private int MAX = 10; //最多10个元素
private int count = 0;
private Lock lock = new ReentrantLock();
//两个Condition,即两个同步队列
private Condition producer = lock.newCondition();
private Condition consumer = lock.newCondition();
public void put(T t) {
try {
lock.lock();
while(lists.size() == MAX) {
producer.await();
}
lists.add(t);
++count;
consumer.signalAll(); //通知消费者线程进行消费
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public T get() {
T t = null;
try {
lock.lock();
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) {
Container2<String> c = new Container2<>();
//启动消费者线程
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();
}
}
}
使用ReentrantLock与synchronized最大区别在于ReentrantLock它可以有两种Condition条件,即会有两个同步队列,一个是成产线程的同步队列,一个是消费者的同步队列,一但数量达到MAX峰值的时候调用producer.await(),使得生产线程全部阻塞,调用consumer.signalAll()唤醒全部消费者线程,也就是说我在producer的情况下阻塞的,我叫醒的只是consumer,同理在消费者线程使得数量为0的时候,我阻塞的全是消费者线程,叫醒的全是生产者线程,这就是ReentrantLock的含义,它能够精确的指定哪些线程被叫醒,我们来说一下Lock和Condition的本质是什么,在synchronized里调用wait()和notify()的时候,它只有一个等待队列,而lock.newnewCondition()的时候,有多少个Condition,则创建了多少个等待队列,Condition的本质就是等待队列个数,以前只有一个等待队列,现在我new了两个Condition,一个叫producer等待队列,另一个叫consumer的等待队列,当我们使用producer.await();的时候,指的是阻塞producer的等待队列中的线程,使用producer.signalAll()指的是唤醒producer这个等待队列的线程,consumner也是如此,在生产者线程里叫醒consumer等待队列的线程也就是消费者线程,在消费者线程里叫醒producer待队列的线程也就是生产者线程。