上篇介绍线程,说到线程同步机制,其中说到加锁的机制,如果加锁不合理,则会产生“死锁”。如果加锁的位置合理,则会解决多线程访问同一数据的问题。多线程访问的问题,其中很典型的一个模型就是生产者/消费者模型,下面就简单介绍一下多线程同步如何模拟生产者/消费者模型。
生产者/消费者模型理解起来并不难,就如社会中的生产消费一样,总要保持一个平衡。生产者需要生产产品,消费者需要消费产品,但是生产者不能不断生产,导致社会中产品偏多;消费者也不能一直消费产品,如果产品被消费完了,则同样会导致不安稳。所以消费者消费和生产者生产之间就得保持平衡,那么线程同步就解决了这个问题。
现在假设有一个容器,生产者生产的产品都放在容器中,消费者消费产品就是从容器中取出。现在生产者和消费者就是两个线程,两个线程会同时访问容器,如果不让容器放满导致产品无法放入,又不让容器为空导致消费者无法消费,就得对容器进行加锁。如果容器为空,则不让消费者线程访问,让生产者去生产;容器满的时候,则不让生产者访问,让消费者去消费,这也就是线程同步。
<span style="font-family:KaiTi_GB2312;font-size:18px;">public class ProducerConsumer {
public static void main(String[] args) {
SyncStack ss = new SyncStack();
Producer p = new Producer(ss);
Consumer c = new Consumer(ss); //p , c两个线程同时访问ss这一个容器
new Thread(p).start();
new Thread(p).start();
new Thread(p).start();
new Thread(c).start();
}
}
//产品类
class WoTou {
int id;
WoTou(int id) {
this.id = id;
}
public String toString() {
return "WoTou : " + id;
}
}
//容器类
class SyncStack {
int index = 0;
WoTou[] arrWT = new WoTou[6];
//生产者生产的产品放到容器中
public synchronized void push(WoTou wt) { //此处是给向容器中放产品的push()方法加锁,关键字为synchronized
while(index == arrWT.length) {
try {
this.wait(); //生产者生产的产品填满容器后,该生产的线程要等着,此时释放该对象的锁,让消费的线程访问
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notifyAll();//生产者生产的产品填满容器后,通知其他线程,也就是通知消费的线程进行操作
arrWT[index] = wt;
index ++;
}
//消费者从容器中取出产品进行消费
public synchronized WoTou pop() { //此处是给从容器中取产品的pop()方法加锁,关键字为synchronized
while(index == 0) {
try {
this.wait(); //消费者消费完容器中的产品后,该线程要等着,此时释放该对象的锁,让生产的线程访问
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notifyAll(); //消费者消费完容器中的产品后,通知其他线程,也就是通知生产的线程进行操作
index--;
return arrWT[index];
}
}
//生产者的线程
class Producer implements Runnable {
SyncStack ss = null;
Producer(SyncStack ss) {
this.ss = ss;
}
//线程启动
public void run() {
for(int i=0; i<20; i++) {
WoTou wt = new WoTou(i);
ss.push(wt);
System.out.println("生产了:" + wt);
try {
Thread.sleep((int)(Math.random() * 200));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//消费者的线程
class Consumer implements Runnable {
SyncStack ss = null;
Consumer(SyncStack ss) {
this.ss = ss;
}
//线程启动
public void run() {
for(int i=0; i<20; i++) {
WoTou wt = ss.pop();
System.out.println("消费了: " + wt);
try {
Thread.sleep((int)(Math.random() * 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}</span>
正如代码中所示,该模型*有五个类,产品类、容器类、生产者线程、消费者线程以及功能测试类。生产者线程和消费者线程是实现了Runnable接口,并且在类里面声明了容器类的对象,各自都是循环着向容器类中放置产品或者取出产品。产品类就是给产品加上id标识,自然也简单不用再说。关键就是在容器类,容器类中既有生产者生产的产品放到容器中的方法,也有消费者从容器中取出产品进行消费的方法。而同步的机制就体现在这里,这两个方法加锁就是解决了多线程访问容器的问题。
在容器类中,有两个关键点,那就是this.wait()和this.notifyAll()。如注释中所示,生产者生产的产品填满容器后,该生产的线程要等着,此时释放容器的锁,让消费的线程访问,而生产者生产的产品填满容器后,通知其他线程,也就是通知消费的线程进行操作。消费者消费完容器中的产品后,该线程也要等着,此时释放容器的锁,让生产的线程访问,消费者消费完容器中的产品后,通知其他线程,也就是通知生产的线程进行操作。
总结
生产者/消费者模型是典型的多线程访问,如果不加锁每个线程可随意访问,则会导致资源错乱,这样每个线程访问资源后得到的都不是自己想要的结果。线程同步机制就是为了避免这种问题,其实本质就是在同一时间只让一个线程访问,其他的等待即可。