0 导言
在Java多线程中有一个经典案例,生产者和消费者(两个线程)对商品(共有对象)进行操作。在本文中,笔者将逐步实现案例,从功能雏形开始逐步完善功能,以保证对Java多线程的同步锁等功能进行深入了解。
在本文中,你将了解Java多线程的以下内容:
- Synchronized关键字 -> 同步锁
- wait 和 notify 方法
- Flag标识符
本案例:
生产者生产商品(赋予商品对象Brand和Name);
消费者购买商品(取走商品的Brand和Name);
1 原始版本
至此,我们完成了对需求的设定,我们可以大致分为以下四类:
- 商品类,包含Brand和Name的属性
- 生产者类,赋予共享的商品类对象以Brand和Name;
- 消费者类,取走商品的Brand和Name;
- 主函数,用于实现程序运行。
基于以上设计方案,我们分别对其进行实现。
1.1 Goods类
//Goods类
public class Goods {
private String brand;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
}
1.2 生产者类
/*
* 生产者任务:
* 生产产品,存入共享空间中。
* */
public class Producer implements Runnable {
private Goods goods;
public Producer(){
}
public Producer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
for( int i = 0 ; i < 10 ; i++){
if ( i % 2 == 0 ){
goods.setBrand("哇哈哈");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
goods.setName("矿泉水");
}else{
goods.setBrand("旺仔");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
goods.setName("小馒头");
}
System.out.println("生产了" + this.goods.getBrand() + this.goods.getName());
}
}
}
1.3 消费者类
/*
* 消费者任务:
* 从共享空间中取走商品
* */
public class Customer implements Runnable{
private Goods goods;
public Customer(){
}
public Customer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
for( int i = 0 ; i < 10 ; i ++){
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者取走了" +
this.goods.getBrand() + "-----" + this.goods.getName());
}
}
}
1.4主函数
/*
* 实现功能:
* 消费者和生产者
* 前者可以从一块区域取走商品
* 后者可以生产商品放入区域
* */
public class Test {
public static void main(String[] args) {
Goods goods = new Goods();
Producer producer = new Producer(goods);
Customer customer = new Customer(goods);
Thread t1 = new Thread(producer);
Thread t2 = new Thread(customer);
t1.start();
t2.start();
}
}
1.5 输出结果
完成实现后,我们会获得输出结果如下(节选):
生产了哇哈哈矿泉水
消费者取走了哇哈哈-----矿泉水
生产了旺仔小馒头
消费者取走了旺仔-----矿泉水
消费者取走了哇哈哈-----小馒头
生产了哇哈哈矿泉水
生产了旺仔小馒头
消费者取走了旺仔-----小馒头
多尝试几次,我们会遇到以下问题:
- 生产和消费的商品的属性不一致(本质上是生产者存储属性到一半的时候,被消费者取走)
- 生产和消费乱序,生产了一个商品却被多次取走
2 解决方案
2.1 问题一:生产和消费的商品的属性不一致
对此,我们想到了同步锁,Synchronized关键字
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;
4. 修改一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
简而言之,被Synchronized关键字修饰的方法,在被一个线程调用的时候,其他线程无法对其进行访问和调用。
这样,我们就可以保证消费者只能在生产者调用结束之后,才能进行消费(即生产者完成商品所有属性的存储,消费者才能取走),这样,消费者就只能在生产者结束生产(完成数据的完整存储之后)才能购买(读取商品的属性),不会再出现商品名不对应的问题。
2.2 问题二:生产和消费乱序,生产了一个商品却被多次取走
对于这个问题,我们首先想到的就是设立一个标识位Flag,用于标识商品是否存在。
- 默认,我们将Flag设为false,即商品不存在。
- 生产者只能在商品不存在的时候生产,并将商品标识符设为true(即已补充货物)
- 消费者只能在商品存在的时候消费,并将商品标识符设为false(即已被买走)
但这样,我们会发现一个新的问题:
由于生产者和消费者访问时间不定,所以很可能出现:
- 消费者买东西的时候,出现没商品的情况,停止购买;
- 生产者生产的时候,发现商品存在,停止生产;
即最终打印的,往往不是20条语句。
那么这就引起我们思考-----------------------------------
当生产者发现商品存在的时候,应该等待商品售完,再进行补货,而不是直接离开店里。
(不应该结束当前方法,应该处于等待状态,当标识符为false的时候,继续访问)
为了实现这一点,就得利用wait和notify方法。
查看源码,我们会发现,这两点不是针对的线程类,而是存在于Object类中,即所有类的父类。
那么让我们来了解以下他们的原理:
void notify()
Wakes up a single thread that is waiting on this object’s monitor.
译:唤醒在此对象监视器上等待的单个线程void notifyAll()
Wakes up all threads that are waiting on this object’s monitor.
译:唤醒在此对象监视器上等待的所有线程void wait( )
Causes the current thread to wait until another thread invokes the notify() method or the notifyAll( ) method for this object.
译:导致当前的线程等待,直到其他线程调用此对象的notify( ) 方法或 notifyAll( ) 方法void wait(long timeout)
Causes the current thread to wait until either another thread invokes the notify( ) method or the notifyAll( ) method for this object, or a specified amount of time has elapsed.
译:导致当前的线程等待,直到其他线程调用此对象的notify() 方法或 notifyAll() 方法,或者指定的时间过完。void wait(long timeout, int nanos)
Causes the current thread to wait until another thread invokes the notify( ) method or the notifyAll( ) method for this object, or some other thread interrupts the current thread, or a certain amount of real time has elapsed.
译:导致当前的线程等待,直到其他线程调用此对象的notify( ) 方法或 notifyAll( ) 方法,或者其他线程打断了当前线程,或者指定的时间过完。
以生产者示例,即:
我们可以在发现商品存在的时候,让线程进行等待。(wait)
直至消费者买走商品,唤醒生产者继续消费。(notify)
至此,我们完成所有的代码,并实现了完整的生产与消费流程。(撒花)
3 完整源码
附上源码如下:
3.1 Goods类
public class Goods {
private String brand;
private String name;
//标识是否存在商品,默认为false
private boolean flag = false;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getBrand() {
return brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
//消费者购买商品
public synchronized void get() {
if ( !this.flag){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者取走了" +
this.getBrand() + "-----" + this.getName());
this.flag = false;
notify();
}
//生产者生产商品
public synchronized void set(String brand, String name) {
if(this.flag) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.setBrand(brand);
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.setName(name);
System.out.println("生产者生产了" + this.getBrand() +
"----" + this.getName());
this.flag = true;
notify();
}
}
3.2 生产者类
public class Producer implements Runnable {
private Goods goods;
public Producer(){
}
public Producer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
for( int i = 0 ; i < 9 ; i++){
if( i % 2 == 0){
goods.set("哇哈哈","矿泉水");
} else {
goods.set("旺仔","小馒头");
}
}
}
}
3.3 消费者类
public class Customer implements Runnable{
private Goods goods;
public Customer(){
}
public Customer(Goods goods) {
this.goods = goods;
}
@Override
public void run() {
for( int i = 0 ; i < 9 ; i++){
goods.get();
}
}
}
3.4 主函数类
public class Test {
public static void main(String[] args) {
Goods goods = new Goods();
Producer producer = new Producer(goods);
Customer customer = new Customer(goods);
Thread t1 = new Thread(producer);
Thread t2 = new Thread(customer);
t1.start();
t2.start();
}
}
强烈推荐各位学习的小伙伴认真看看本文中附上的两篇博客。
当然,你也可以选择免费下载本文逐步实现的源码: