生产者消费者模式之synchronized与Object

    我在《同步之synchronized关键字中举了一个春运卖票的例子,在这个例子中实现了使用线程来同时运行多个任务时,通过使用锁(互斥)来同步两个任务的行为,从而使得一个线程不会干涉另一个线程的资源。也就是说,如果两个线程在交替着步入某项共享资源,可以使用互斥来使得任意时刻只有一个线程可以访问这项资源。

    虽然这个问题已经得到了解决,但是现实中卖票应该是这样的:首先先产生了票,然后才能售票。也就是说多个线程应该彼此之间可以协作,所以在这个问题中,必须先生成票然后才能售票,推广到一般性的概述也就是说,要求某些部分必须在其他部分被解决之前解决。生产者消费者问题就是这类问题的经典体现。该问题描述了两个共享固定大小缓冲区的线程,即所谓的“生产者”与“消费者”在实际运行中会发生的问题。

    生产者的主要作用是生产一定数量的资源放在缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消费这些资源。该问题的关键就是要保证生产者不会在缓冲区满时生产资源,消费者也不会在缓冲区空时消费资源。所以要解决该问题,关键就是要设法让生产者在缓冲区满时等待,等到下次消费者消费缓冲区中的资源时生产者才能被唤醒,继续往缓冲区中加入资源。同理,消费者也应该在缓冲区空时等待,等到生产者往缓冲区中加入资源后再唤醒消费者消费。



    有了处理思路后,要怎样实现呢?代码应该怎样体现出来?别担心,java的等待唤醒机制就可以有效的解决这个问题。在Object类中有三个方法wait()notify()/notifyAll()可供我们使用。

    先来了解wait() notify()/notifyAll()的作用:

    wait():可以等待某个条件发生变化,而改变这个条件超出了当前方法的控制力。通常这个条件将由另一个线程来改变。因此wait()会在等待外部世界产生变化的时候将线程挂起并释放锁(这一点与sleep()不同),并且只有在notify()notifyAll()发生时,这个线程才会被唤醒并去检查所产生的变化。

    notify():唤醒由于wait()而等待的任意一个线程。

    notifyAll():唤醒由于wait()而等待的所有线程。

    我们还是以卖票为例来体现生产者消费者模式的应用,假设有一个生产者线程和一个消费者线程,生产者线程用于生产特定班次的车票,消费者线程用于消费(购买)特定班次的车票。而且限制如果没有车票的话,消费者线程就必须等待而不能消费,同时唤醒生产者线程生产车票;如果已经有了车票,生产者线程就必须等待而不能生产车票,同时唤醒消费者线程进行消费。使用JDK1.5之前传统的线程通信方式如下: 

      创建资源对象类Ticket


package com.gk.thread.communication;

public class Ticket {
	
	private String place;	// 车票的起始地
	private String date;	// 车票的日期
	private int number;		// 车票的数量
	public boolean empty = true;	// 标记,是否有车票,true表示没有车票
	
	public String getPlace() {
		return place;
	}
	public void setPlace(String place) {
		this.place = place;
	}
	public int getNumber() {
		return number;
	}
	public void setNumber(int number) {
		this.number = number;
	}
	public String getDate() {
		return date;
	}
	public void setDate(String date) {
		this.date = date;
	}
	public boolean isEmpty() {
		return empty;
	}
	public void setEmpty(boolean empty) {
		this.empty = empty;
	}
	
	@Override
	public String toString() {
		return "Ticket [place=" + place + ", number=" + number + ", date=" + date + ", empty=" + empty + "]";
	}

}


    创建生产者类Producer


package com.gk.thread.communication.synchronization;

import java.text.SimpleDateFormat;
import java.util.Date;

import com.gk.thread.communication.Ticket;

public class Producer implements Runnable {

	private Ticket ticket;
	
	public Producer(Ticket ticket) {
		
		this.ticket = ticket;
	}
	
	@Override
	public void run() {
		
		/*
		 * 每次进入run方法之后加锁,然后判断有没有票,true表示没有车票
		 */
		synchronized(ticket) {
			
			if(!ticket.isEmpty()) {
				
				try {
					ticket.wait();		// 如果有票则等待
				} catch (InterruptedException e) {
					throw new RuntimeException(e);
				}
			}
			
			/////////////////////////////////////
			/*
			 * 如果没有票则生产票
			 */
			String place = Thread.currentThread().getName();	// 获取线程名,表示车票的起始地
			String date = new SimpleDateFormat("yyyy-MM-dd HH点mm分").format(new Date());	// 车票的时间
			int number = (int)(Math.random()*10 + 100);		// 随机生成车票张数,在100-110之间(不包括110)
				
			/*
			 * 设置车票的属性
			 */
			ticket.setPlace(place);
			ticket.setDate(date);
			ticket.setNumber(number);
				
			System.out.println("生产了" + number + "张  " + date + "  " + place + "的票...\n");
			
			
			ticket.setEmpty(false);		// 生产者生产了票之后就有票了,所以修改标记empty为false
			ticket.notify();		// 唤醒等待着的消费者消费(购买)车票
				
		}

	}

}

        创建消费者类Customer


package com.gk.thread.communication.synchronization;

import com.gk.thread.communication.Ticket;

public class Customer implements Runnable {

	private Ticket ticket;
	
	public Customer(Ticket ticket) {

		this.ticket = ticket;
	}
	
	@Override
	public void run() {

		/*
		 * 每次进入run方法之后加锁,然后判断有没有票,true表示没有车票
		 */
		synchronized (ticket) {
			
			if(ticket.isEmpty()) {
				
				try {
					ticket.wait();		// 如果没有票则等待
				} catch (InterruptedException e) {
					throw new RuntimeException(e);
				}
			}

			/////////////////////////////////////
			/*
			 * 如果有票则消费票
			 */
			String place = ticket.getPlace();
			String date = ticket.getDate();
			int number = ticket.getNumber();
				
			System.out.println("消费了" + number + "张  " + date + "  " + place + "的票...\n");
			
			ticket.setEmpty(true);		// 消费者消费了票之后就没有票了,所以修改标记empty为true
			ticket.notify();		// 唤醒等待着的生产者生产车票
			
		}
	}

}


    测试代码:


package com.gk.thread.communication.synchronization;

import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

import com.gk.thread.communication.Ticket;

public class Test {
	
	public static void main(String[] args) {
		
		/*
		 * 因为生产者线程和消费者线程是针对同一种资源进行的操作,
		 * 所以在外界创建同一资源对象Ticket,
		 * 然后再将此Ticket分别传递给生产线线程Producer和消费者线程Customer
		 */
		Ticket ticket = new Ticket();		
		
		Runnable producer = new Producer(ticket);
		Runnable customer = new Customer(ticket);
		
		new Thread(customer).start();	
		new Thread(producer, "广州  --->  北京").start();
		
	}

}

     在生产者类Producer中,每次进入run方法先加上锁,然后判断有没有车票,如果有车票则wait()等待,同时释放锁;如果没有车票则生产车票,同时notify()唤醒等待着的消费者线程消费。

    消费者与生产者正好相反,在消费者类Customer,每次进入run方法先加上锁,然后判断有没有车票,如果没有车票则wait()等待,同时释放锁;如果有车票则消费车票,同时notify()唤醒等待着的生产者线程生产

    需要注意的是,因为生产者线程和消费者线程都是对车票这同一种资源进行的操作,所以我们不能在生产者线程中创建一个Ticket对象同时又在消费者线程中创建一个Ticket对象,而是要设法使生产者线程和消费者线程使用同一个Ticket对象,所以我们就在外界创建一个Ticket对象然后通过构造器传递给生产者线程和消费者线程,从而达到使生产者和消费者使用同一种资源的目的。

    上面测试的是一个生产者线程和一个消费者线程的情况,但是当有多个生产者和多个消费者的时候还能满足吗?

    修改测试代码:


package com.gk.thread.communication.synchronization;

import java.util.List;
import java.util.ArrayList;
import java.util.Collections;

import com.gk.thread.communication.Ticket;

public class Test {
	
	public static void main(String[] args) {
		
		Ticket ticket = new Ticket();	
		
		Runnable producer = new Producer(ticket);
		Runnable customer = new Customer(ticket);
		
		// 一个生产者一个消费者的情况
		/*new Thread(customer).start();
		new Thread(producer, "广州  --->  北京").start();*/
		
		
		// 多个生产者多个消费者的情况
		String from = "广州";
		String[] to = {"北京", "上海", "长沙", "杭州", "重庆", "西安", "厦门", "拉萨", "西藏", "哈尔滨"};
		List<String> place = new ArrayList<String>();
		for(int i=0; i<to.length; i++) {
			
			place.add(from + "  --->  " + to[i]);
		}
		
		for(String p : place) {
			new Thread(producer, p).start();	// 生产者
			new Thread(customer).start();		// 消费者
		}
		
	}
}


    从运行结果中我们就可以看出已经出错了,出现了一次生产多次消费或一次生产没有消费的情况。那么究竟是在哪里引起的呢?我在上面说过当线程调用wait()方法时会同时释放此线程拥有的锁,然后其他线程又开始抢CPU的执行权,有可能多个消费者线程都抢到了,不过由于在if块中判断没有票所有都在wait()等待,当生产者线程抢到CPU的执行权生产了票唤醒了等待的消费者线程后,那些等待的消费者线程被唤醒之后没有再去判断标记(因为是if语句只判断一次)就往下执行后边的代码了,从而也就导致了生产一次消费多次的情景了。生产一次没有消费的情况也类似。

    要解决此问题就要设法让线程被唤醒之后不是直接执行后边的代码,而是回去判断标记,通过将if修改成while就行了。但是这样做有可能会出现死锁的现象:notify()唤醒的是任意一个线程,不能保证唤醒的是对方线程,如果唤醒的是本类型的线程,就会导致所有线程全部等待。所以要将notify()换成notifyAll()

    修改生产者类Producer。为了减少篇幅,这里只显示要修改的run方法,其他代码是一样的。


@Override
public void run() {
		
	synchronized(ticket) {
			
			/*if*/while(!ticket.isEmpty()) {
				
				try {
					ticket.wait();		
				} catch (InterruptedException e) {
					throw new RuntimeException(e);
				}
			}
			
			/////////////////////////////////////
			String place = Thread.currentThread().getName();	
			String date = new SimpleDateFormat("yyyy-MM-dd HH点mm分").format(new Date());	
			int number = (int)(Math.random()*10 + 100);				
			
			ticket.setPlace(place);
			ticket.setDate(date);
			ticket.setNumber(number);
				
			System.out.println("生产了" + number + "张  " + date + "  " + place + "的票...\n");
			
			
			ticket.setEmpty(false);
			//ticket.notify();		
			ticket.notifyAll();
	}		
}


    消费者类Customer要修改的地方跟生产者类Producer一样,这里就不放上来了。


    还是运行上面修改后的测试代码



    终于搞定了,但是不知道大家有没有这样一个疑惑?等待唤醒机制的wait()、 notify()/notifyAll()方法都是与线程相关的方法,为什么不定义在Thread类中而是定义在Object类中呢?是不是感觉很奇怪——仅仅针对线程的功能却作为通用类Object的一部分而实现。不过仔细想想这是有道理的,因为这些方法操作的锁也是所有对象的一部分。所以我们可以把wait()方法放进任何同步方法或同步代码块中,而不用考虑这个类是继承自Thread还是实现了Runnable接口。

    实际上,obj.wait()obj.notify()/obj.notifyAll()必须要与synchronized(obj)一起使用,也就是wait()与notify()/notifyAll()是针对已经获取了obj锁进行的操作。从语法的角度来说就是只能在同步方法或同步代码块中调用wait()和 notify()/notifyAll()方法。如果不是在同步方法或同步代码块中调用这些方法,程序能通过编译,但是运行的时候,将会得到IllegalMonitorStateException异常。从功能上来说就是wait()在获取对象锁之后,主动释放对象锁,同时本线程休眠,直到有其它线程调用对象的notify()/notifyAll()唤醒该线程才能继续获取对象锁,并继续执行。但是有一点需要注意的是notify()/notifyAll()调用之后并不是马上就释放对象锁,而是在相应的synchronized(obj){...}代码块执行结束后才释放的。

    终于写完了,本来不想写太多文字的,但还是废话了好多。希望本文能对你有帮助,也希望能指出文中的不足之处或其中的错误,大家一同学习。最后再废话一句。

    好奇心驱使我们成长,读者可以试着把wait()和 notify()/notifyAll()方法注释掉看看程序运行之后会出现怎样的结果。将synchronized去掉呢。



猜你喜欢

转载自blog.csdn.net/leexichang/article/details/79328564
今日推荐