转载自:http://blog.csdn.net/fengzhe0411/article/details/6949622
前言:自己尝试着用java多线程实现了操作系统原理中讲到的“生产者-消费者”模型,在这里和大家分享一下遇到的问题和心得。我们姑且模糊“线程”和“进程”的区别,只记住它们都是可并发执行的一组过程即可。
一、什么是“生产者-消费者”模型?
这个模型所描述的是假设有一个能容纳N个产品的工厂,生产者进程不断向工厂中输入产品,而消费者进程不断从工厂中取走产品。尽管所有的生产者和消费者都是已异步的方式运行的,但它们之间必须保持同步,即不允许消费者进程到一个空的工厂中取产品,也不允许生产者进程向一个已经装满产品的工厂投放产品。
二、用java多线程实现“生产者-消费者”模型(详细代码)
package cn.uestc.fz;
public class ProducerConsumer {
public static void main(String[] args) {
Factory factory = new Factory(0);
ProducerThread pt1 = new ProducerThread(factory, 10);
ProducerThread pt2 = new ProducerThread(factory, 10);
ProducerThread pt3 = new ProducerThread(factory, 10);
ConsumerThread ct1 = new ConsumerThread(factory, 10);
ConsumerThread ct2 = new ConsumerThread(factory, 10);
ConsumerThread ct3 = new ConsumerThread(factory, 10);
pt1.setName("生产者1号");
pt2.setName("生产者2号");
pt3.setName("生产者3号");
ct1.setName("消费者1号");
ct2.setName("消费者1号");
ct3.setName("消费者1号");
pt1.start();
pt2.start();
pt3.start();
ct1.start();
ct2.start();
ct3.start();
}
}
/*工厂类*/
class Factory{
private static final int MAX_NUMBER=100;
private int currentNumber;
public Factory(int currentNumber) {
super();
this.currentNumber = currentNumber;
}
public void add(int produceNumber){
//同步方法,保证向工厂添加产品操作是互斥的
synchronized(this){
while(currentNumber+produceNumber>MAX_NUMBER){
System.out.println(Thread.currentThread().getName()+":当前生产过剩!无法投放!");
try {
wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
currentNumber+=produceNumber;
System.out.println(Thread.currentThread().getName()+":投放了"+produceNumber+"个产品,当前工厂里产品的数量为:"+currentNumber);
notifyAll();
}
try {
Thread.sleep(100);//休眠0.1秒,好让其它线程有机会运行
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void get(int consumeNmber){
//同步方法,保证从工厂消费产品操作是互斥的
synchronized(this){
while(currentNumber
运行结果:
生产者1号:投放了10个产品,当前工厂里产品的数量为:10
生产者2号:投放了10个产品,当前工厂里产品的数量为:20
消费者1号:消费了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
消费者1号:工厂剩余产品不足!无法消费!
生产者3号:投放了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
生产者2号:投放了10个产品,当前工厂里产品的数量为:10
生产者1号:投放了10个产品,当前工厂里产品的数量为:20
消费者1号:消费了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
生产者3号:投放了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
生产者2号:投放了10个产品,当前工厂里产品的数量为:10
生产者1号:投放了10个产品,当前工厂里产品的数量为:20
消费者1号:消费了10个产品,当前工厂里产品的数量为:10
生产者3号:投放了10个产品,当前工厂里产品的数量为:20
消费者1号:消费了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
生产者1号:投放了10个产品,当前工厂里产品的数量为:10
生产者2号:投放了10个产品,当前工厂里产品的数量为:20
消费者1号:消费了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
生产者3号:投放了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
生产者1号:投放了10个产品,当前工厂里产品的数量为:10
生产者2号:投放了10个产品,当前工厂里产品的数量为:20
消费者1号:消费了10个产品,当前工厂里产品的数量为:10
生产者3号:投放了10个产品,当前工厂里产品的数量为:20
消费者1号:消费了10个产品,当前工厂里产品的数量为:10
消费者1号:消费了10个产品,当前工厂里产品的数量为:0
生产者2号:投放了10个产品,当前工厂里产品的数量为:10
......
理想状态下此程序应该会源源不断的运行着。
三、关于操作系统中的进程(or线程)转换的三种状态
现代操作系统一般采用分时操作系统,也就是说进程(or线程)在计算机上运行都是分时间片的,计算机的时间被划分为一个个细小的时间片,分给需要运行的进程(or线程)用。这里必须要清楚的是,在单CPU的计算机上,同一时间只能有一个进程(or线程)在运行,这就是我们说的“并发”,而并不是“并行”。一旦某个进程(or线程)用完时间片之后,它会退回到“就绪”状态,而如果运行的中途出现问题,如缺少资源而导致无法运行的时候,它就会变成阻塞的状态。这个时候操作系统的调度程序会根据某种调度算法来将就绪队列中的一个进程(or线程)提升为运行状态。而那个被阻塞的进程(or线程)在得到它所缺少的资源后会变为就绪状态,等待调度程序把它调度执行。
四、为什么要用Thread类的静态方法sleep让当前的进程休眠?
我们知道,其实各个进程(or线程)就是切换着运行的,但是为了让我们观察的更明显,每次完成一次取出或者投放操作,都让当前的线程休眠掉,这样子其它线程就有就会执行。当当前的线程执行sleep操作后,相当于把它给阻塞掉了(此时这个线程的时间片也许并没有用完),sleep的参数是毫秒。也就是说这个毫秒的时间过后,操作系统会给此线程一个信号(相当于缺少的资源)说你的时间到了,然后把它变为就绪状态等待新一轮的调度。(注意就算sleep后的时间到了,该线程可能也不会马上运行,只能说它有了运行的机会)
五、为什么Factory类中add和get方法要加synchronized同步块?
这里就要介绍操作系统中“临界区”的概念了,“临界资源”指的是各进程互斥的共享的资源,也就是我有的时候你不能有,你有的时候我不能有的东西,但是当我没有了,你就可以有了。而“临界区”就是对“临界资源”访问的那段代码,显然我们要求一次只能有一个进程或者线程访问这段代码。在java中每个对象都有唯一的一把“锁”,当某个线程得到这把锁的时候,其它线程只能在外面徘徊,直到这个线程放弃这把锁,其它等待锁的线程再去竞争这把锁。
那么如果我们不使用synchronized同步块会怎么样呢?如果当前工厂中有0个产品,这时有一个生产者线程pt1执行了“currentNumber+=produceNumber”的操作,碰巧这个时候线程发生切换,另一个生产者线程pt2也执行了“currentNumber+=produceNumber”的操作,然后线程再次切换回pt1,此时打印出来的是“生产者1号:投放了10个产品,当前工厂里产品的数量为:20”,其实正确的应该结果是“10”。这显然是不对的。
而当加了synchronized同步块后,线程pt1拥有factory对象的锁,此时pt2是无法运行这段代码的,它只能等待pt1释放了这把锁再去执行。
六、wait方法和notifyAll方法是怎么用的?
这几个方法都是存在于Object类中的。
public final void wait() throws InterruptedException
notify()
方法或 notifyAll()
方法前,导致当前线程等待。换句话说,此方法的行为就好像它仅执行 wait(0) 调用一样。
当前线程必须拥有此对象监视器。该线程发布对此监视器的所有权并等待,直到其他线程通过调用 notify
方法,或 notifyAll
方法通知在此对象的监视器上等待的线程醒来。然后该线程将等到重新获得对监视器的所有权后才能继续执行。
对于某一个参数的版本,实现中断和虚假唤醒是可能的,而且此方法应始终在循环中使用:
synchronized (obj) { while (此方法只应由作为此对象监视器的所有者的线程来调用。有关线程能够成为监视器所有者的方法的描述,请参阅) obj.wait(); ... // Perform action appropriate to condition }
notify
方法。
public final void notify()
wait
方法,在对象的监视器上等待。
直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
此方法只应由作为此对象监视器的所有者的线程来调用。通过以下三种方法之一,线程可以成为此对象监视器的所有者:
synchronized
语句的正文。 Class
类型的对象,可以通过执行该类的同步静态方法。 一次只能有一个线程拥有对象的监视器。
public final void notifyAll()
wait
方法,在对象的监视器上等待。
直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。
此方法只应由作为此对象监视器的所有者的线程来调用。有关线程能够成为监视器所有者的方法的描述,请参阅 notify
方法。
首先我们必须明白,以上几个方法必须在同步方法或者同步代码块中调用,否则会抛出java.lang.IllegalMonitorStateException的异常。也就是说,在线程执行以上几个方法的时候,它必须拥有该对象的锁(一次只能有一个线程拥有对象的监视器)。
当在对象上调用wait方法时,执行该代码的线程立即放弃它在对象上面的锁。然后当调用notify时,并不意味着调用notify的线程会立即放弃其锁。如果此线程仍然在完成同步代码,则线程在执行完这段代码后才会放弃其锁。因此,调用notify方法时并不意味着这时该锁变得可用。我们可以看到API上面的解释是:直到当前线程放弃此对象上的锁定,才能继续执行被唤醒的线程。参考:http://blog.csdn.net/iceman1952/article/details/2159812
多线程之间需要协调工作。例如,浏览器的一个显示图片的线程displayThread想要执行显示图片的任务,必须等待下载线程 downloadThread将该图片下载完毕。如果图片还没有下载完,displayThread可以暂停,当downloadThread完成了任务 后,再通知displayThread“图片准备完毕,可以显示了”,这时,displayThread继续执行。
以上逻辑简单的说就是:如果条件不满足,则等待。当条件满足时,等待该条件的线程将被唤醒。在Java中,这个机制的实现依赖于wait/notify。等待机制与锁机制是密切关联的。例如:
线程A:
synchronized(obj) {
while(!condition) {
obj.wait();
}
obj.doSomething();
}
当线程A获得了obj锁后,发现条件condition不满足,无法继续下一处理,于是线程A就wait()。
在另一线程B中,如果B更改了某些条件,使得线程A的condition条件满足了,就可以唤醒线程A:
线程B:
synchronized(obj) {
condition = true;
obj.notify();
}
需要注意的概念是:
1、调用obj的wait(), notify()方法前,必须获得obj锁,也就是必须写在 synchronized(obj) {...}代码段内。
即:
obj.wait(), obj.notify()必须在synchronized(obj){}内部,也就是说,必须得先得到锁才可以wait, nofity
2、主调obj.wait()的线程(线程A),在obj.wait()执行时,释放掉锁
3、线程A再次运行时,必须要得到锁,而且,这次运行是从obj.wait()后的第一条语句开始的(当然不包括obj.wait())
4、如果A1,A2,A3都在obj.wait(),则B调用obj.notify()只能唤醒A1,A2,A3中的一个(具体哪一个由JVM决定)。
5、obj.notifyAll()则能全部唤醒A1,A2,A3,但是要继续执行obj.wait()的下一条语句,必须获得obj锁,因此,A1,A2,A3只有一个有机会获得锁继续执行,例如A1,其余的需要等待A1释放obj锁之后才能继续执行。
6、当B调用obj.notify/notifyAll的时候,B正持有obj锁,因此,A1,A2,A3虽被唤醒,但是仍无法获得obj锁。直到B退出synchronized块,释放obj锁后,A1,A2,A3中的一个才有机会获得锁继续执行。
七、notify和notifyAll的区别
notify就是在所有wait的线程中选择一个,使它被唤醒;
notifyAll就是使所有wait的线程被唤醒。
那么问题就来了,当notify所在的那段同步代码执行完后,释放掉该对象的锁,那么notify唤醒的那个线程就一定会得到该锁吗?我在网上看到很多都是这样说的,我认为这是错误的。我们应该明确,“唤醒”和“得到锁”不是一个概念,“唤醒”只是有机会得到锁罢了,虽然notify只是唤醒一个线程,但是别忘了,可能还有其它的线程要进入synchronized同步块,它们也有可能获得这把锁。API中也说了:被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争。
简单点说:notify使一个wait的线程唤醒,它有机会得到锁,其它wait的线程连得到锁的机会都没;notifyAll使所有wait的线程唤醒,它们都有机会得到锁。
八、为什么条件判断要用while而不用if
我先开始写的时候用的是if,觉得你如果不满足条件,就去等待嘛,然后等着被唤醒然后得到锁。但是别忘了,就算你得到了锁,只是代表你可以运行,并不意味着条件就满足了。比如说,生产者线程1发现工厂满了,等待之,生产者线程2也发现满了,等待,消费者1消费了之后执行notifyALL操作,生产者1和2都被唤醒,1得到了锁执行,它也调用notifyAll,这时2得到了锁,但是它能往工厂添加产品吗?此时工厂又满了啊,所以每次被唤醒,都要再次进行条件判断。
wait相当于是释放锁之后进入阻塞队列,当被唤醒,且为可执行的时候,就会继续从调用wait方法的那个地方执行。
这个可能有点绕,简单点说,就是唤醒你的线程并不一定是和你业务逻辑相关的线程,上面的生产者2就是被生产者1唤醒的,而我们想象中可能认为它是被某个消费者线程唤醒的。
总结下多线程的同步:
线程的同步分为两种方式:同步块和同步方法
1、同步块
synchronized(object)
{
}
同步块所同步的对象为object参数,获取的是一个对象实例的锁;
要保证在同步object这个对象实例的时候,保证object该引用指向的对象实例是同一个,不然不能获得预期的效果,因为object该引用指向的对象实例若有变化,则同步块获取的锁就不是同一把锁了。
2、同步方法
public [static] synchronized void test()
{
}
方法分为静态方法和非静态方法
若是非静态同步方法,则获取的锁相当于是调用该方法的this实例(和同步块的synchronized(this){}效果一样),同样,你要保证每次调用该同步方法的是同一个实例(即同一个this),就像上例中的生产者消费者问题中只有一个factory实例一样。
你若new出很多实例,然后分别调用非静态同步方法,则获取的this的锁不同,同步会失败
若是静态同步方法,对象锁就是该静态方法所在的类的Class(ClassName.class)实例,由于在JVM中,所有被加载的类都有唯一的类对象。不管我们创建了该类的多少实例,但是它的类实例仍然是一个!
你若new出很多实例,然后分别调用静态同步方法,仍然是可以同步的
所以,同步其实上是对临界资源的一种互斥操作,既要达到以同步互斥的方式保护临界资源的目的,同时要明确临界资源的唯一性,对同一临界资源进行操作的同步代码块之间使用的锁的唯一性(使用同一个锁)。竞争同步锁失败的线程进入的是该同步锁的就绪队列。
关于线程同步,需要牢牢记住的第一点是:线程同步就是线程排队。同步就是排队。线程同步的目的就是避免线程“同步”执行。这可真是个无聊的绕口令。
关于线程同步,需要牢牢记住的第二点是 “共享”这两个字。只有共享资源(即临界资源)的读写访问才需要同步。如果不是共享资源,那么就根本没有同步的必要。
关于线程同步,需要牢牢记住的第三点是,只有“变量”才需要同步访问。如果共享的资源是固定不变的,那么就相当于“常量”,线程同时读取常量也不需要同步。至少一个线程修改共享资源,这样的情况下,线程之间就需要同步。
关于线程同步,需要牢牢记住的第四点是:多个线程访问共享资源的代码有可能是同一份代码,也有可能是不同的代码;无论是否执行同一份代码,只要这些线程的代码访问同一份可变的共享资源,这些线程之间就需要同步。