问题:
写一个固定容量同步容器,拥有put和get方法,以及getCount方法,能够支持2个生产者线程以及5个消费者线程的阻塞调用。
方案一:
1.使用synchronized、notifyAll、wait实现
public class PSproblem_01<T> {
final private LinkedList<T> lists = new LinkedList<>();
final private int MAX = 10; //最多10个元素
//生产过程
public synchronized void put(T t) {
while (lists.size() == MAX) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
lists.add(t);
System.out.println("生产者" + Thread.currentThread().getName() + "生产了" + t);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.notifyAll();
}
//消费过程
public synchronized T get() {
T t = null;
while (lists.size() == 0) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
t = lists.remove();
System.out.println("消费者" + Thread.currentThread().getName() + "消费了" + t);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.notifyAll();
return t;
}
public static void main(String[] args) {
PSproblem_01<String> container = new PSproblem_01<>();
//启动2个生产者线程
for (int i = 1; i <= 2; i++) {
int index = i;
new Thread(() -> {
int count = 0;
while (count++ < 5) {
container.put("包子");
}
}, Integer.toString(index)).start();
}
//启动5个消费者线程
for (int i = 1; i <= 5; i++) {
int index = i;
new Thread(() -> {
int count = 0;
while (count++ < 2) {
container.get();
}
}, Integer.toString(index)).start();
}
}
}
3.疑问
为什么判断条件用while而不用if?
先来了解一下两个概念,以便更容易理解问题答案。
- 锁池:
假设线程A已经拥有了某个对象的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。 - 等待池:
假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入 到了该对象的等待池中。 - 当有线程调用了对象的notifyAll()方法(唤醒所有wait线程)或notify()方法(只随机唤醒一个wait线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争
假设我们遇到这样情况:
生产者线程1获取锁后,容器容量判断已经到达MAX,所以线程1执行wait()后进入释放当前对像锁,进入等待池中。随后又一个生产者线程2获得锁,由于容量还是MAX,所以线程2也释放锁进入等待池。后面消费者线程3消费了一次,执行notifyAll唤醒了当前对象的所有等待池的线程。这个时候线程1抢到了锁,进行一次生产,但是可能下次抢到锁的是线程2,这时线程2上次wait之后的代码将继续执行下去,进行了一次生产,但是此时容器容量已经是11个了。如果我们用while的话,线程3这次会重新判定一次容量是否满了,这样满了又会进入等待池,直到后面满足条件才进行生产。
所以我们这里必须用while,而不能用if。
方案二(推荐):使用Condition
使用Lock和Condition来实现, 对比两种方式,Condition的方式可以更加精确的指定哪些线程被唤醒。
代码:
public class PSproblem<T> {
final private LinkedList<T> lists = new LinkedList<>();
final private int MAX = 10; //最多10个元素
ReentrantLock lock = new ReentrantLock();
Condition producer = lock.newCondition();//用于生产线程
Condition consumer = lock.newCondition();//用于消费线程
//消费过程
public T get() {
T t = null;
try {
lock.lock();
while (lists.size() == 0) {
try {
consumer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//模拟消费
t = lists.removeFirst();
System.out.println("消费者" + Thread.currentThread().getName() + "消费产品");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
producer.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return t;
}
//生产过程
public void put(T t) {
try {
lock.lock();
while (lists.size() == MAX) {
try {
producer.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//模拟生产
lists.add(t);
System.out.println("生产者" + Thread.currentThread().getName() + "生产了产品");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
consumer.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
PSproblem<String> container = new PSproblem<>();
//启动2个生产者线程
for (int i = 1; i <= 2; i++) {
int index = i;
new Thread(() -> {
int count = 0;
while (count++ < 5) {
container.put("包子");
}
}, Integer.toString(index)).start();
}
//启动5个消费者线程
for (int i = 1; i <= 5; i++) {
int index = i;
new Thread(() -> {
int count = 0;
while (count++ < 2) {
container.get();
}
}, Integer.toString(index)).start();
}
}
}