第23章 java线程通信——生产者/消费者模型案例

第23章 java线程通信——生产者/消费者模型案例

1.案例:

package com.rocco;

/**
* 生产者消费者问题,涉及到几个类
* 第一,这个问题本身就是一个类,即主类
* 第二,既然是生产者、消费者,那么生产者类和消费者类就是必须的
* 第三,生产什么,消费什么,所以物品类是必须的,这里是馒头类
* 第四,既然是线程,那么就不是一对一的,也就是说不是生产一个消费一个,既然这样,多生产的往哪里放
* 现实中就是筐了,在计算机中也就是数据结构,筐在数据结构中最形象的就是栈了,因此还要一个栈类
*/ public class ProduceConsume {
public static void main(String[] args) {
SyncStack ss = new SyncStack();//建造一个装馒头的框
Producer p = new Producer(ss);//新建一个生产者,并把已经创建好框传给它,使其使用这个框
Consume c = new Consume(ss);//新建一个消费者,并把已经创建好框传给它,使其使用这个框
Thread tp = new Thread(p);//接口方法创建一个生产者线程
Thread tc = new Thread(c);//接口方法创建一个消费者线程
tp.start();//开启生产者线程
tc.start();//开启消费者线程
}
} //馒头类
class SteamBread{
int id;
SteamBread(int id){
this.id = id;
} @Override
public String toString() { //重写馒头返回的方法
return "SteamBread:" + id;
}
} //装馒头的框,栈结构
class SyncStack{
int index = 0;
SteamBread[] stb = new SteamBread[6]; //构造一个装馒头的数组,容量是6 //放入框中,相当于入栈
public synchronized void push(SteamBread sb){
while (index==stb.length){ //筐满了,即栈满,
try {
this.wait();//让当前线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify();//唤醒在此对象监视器上等待的单个线程,即消费者线程
stb[index]=sb;
this.index++;
} //从框中拿出,相当于出栈
public synchronized SteamBread pop(){
while (index==0){//筐空了,即栈空
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify();
this.index--;//push第n个之后,this.index++,使栈顶为n+1,故return之前要减一
return stb[index];
}
} //生产者类,实现了Runnable接口,以便于构造生产者线程
class Producer implements Runnable{
SyncStack ss = null;
Producer(SyncStack ss){
this.ss = ss;
} @Override
public void run() {
// 开始生产馒头
for (int i = 0; i < 20; i++) {
SteamBread stb = new SteamBread(i);//生产一个新的包子
ss.push(stb);//把包子存到框里面
System.out.println("生产了"+stb);//打印出我们生产了一个包子,注意此处stb本身是有一个返回值的,这这里将被调用
try {
Thread.sleep(10);//每生产一个馒头,睡觉10毫秒
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} //消费者类,实现了Runnable接口,以便于构造消费者线程
class Consume implements Runnable{
SyncStack ss = null;
public Consume(SyncStack ss){
super();
this.ss = ss;
} @Override
public void run() {
//开始消费馒头
for (int i = 0; i < 20; i++) {
SteamBread stb = ss.pop();
System.out.println("消费了"+stb);
try {
Thread.sleep(10);//每消费一个馒头,睡觉100毫秒。即生产多个,消费一个
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

代码版权是thinkpadshi博主的,写的挺好的。

2.知识点讲解

线程通信-wait()和notify()方法介绍:

java.lang.bjec类提供了这两类方法用于线程通信

wait():执行该方法的线程对象释放同步锁,JVM把该线程存放到等待池中,等待其他线程唤醒该线程

notify():执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待

notifyAll():执行该方法的线程唤醒在等待池中等待的所有线程,把线程转到锁池中等待

注意:上述方法只能被同步监听锁对象那个来调用,否则报错:

建设A线程和B线程共同操作一个X对象。A,B线程可以通过X对象的wait()和notify()方法来进行通信,流程如下:

1.当A线程执行X对象的同步方法时,A线程持有X对象的锁,B线程在X对象的锁池中等待

2.A线程在同步方法中执行X.wait()方法时,A线程释放X对象的锁,并进入X对象的等待池

3.在X对象的锁池中等待的B线程获取X兑现的锁,执行X的另一个同步方法

4.B线程在同步方法中执行X.notify()方法时,JVM把A线程从X对象的等待池中移动到X对象的锁池中,等待获取锁

5.B线程执行完同步方法,释放锁A线程获得锁,继续执行同步方法

解释一下:

等待池指的是线程现在还没有能力去抢锁,所以被放在一边被等待赋予抢锁的能力。

有点像你投简历找工作需要经历简历和面试两个关卡,简历刚投到一家公司的时候,你现在还不具备去面试的资格,这个时候你的简历被放在一堆不被面试的文件夹里,这个文件夹叫做等待池文件夹

锁池就是线程具备了抢锁的能力,但是同时有多个线程来抢,这个时候线程就处在锁池里,然后等待谁抢到锁,谁就执行锁里面的的代码块。

同样,就像你的简历通过的筛选,通知你参加面试了,这个时候你就具备了争夺这个工作的机会,但是这个时候还是有很多的人跟你一样有面试机会,但最终只能录取一个人,至于录用谁就要看这个人的能力了。这个时候你的简历从等待池里面拿出来了,放在锁池里面。

为什么wait(),notify()都是Object类的方法:

多个线程只有使用相同的一个对象的时候,多线程之间才有互斥效果

我们把这个用来做互斥的对象称之为:同步监听对象/同步锁

同步锁可以选择任意类型的对象即可,只需要保证多个线程使用的是相同锁对象即可

因为,只有同步监听对象才能调用wait和notify方法,所以wait和notify方法应该存在与Object类中,而不是Thread类中

3.使用Lock(锁)方法

上面是使用普通的synchronized修饰符,多线程的同步操作有三种方式,现在看看Lock方法如何来写

使用Lock和Condition接口:

wait()和notify()方法,只能被同步监听锁对象来调用,否则报错IllegalMontiorStateException

那么,现在有一个问题,Lock机制是不需要同步锁的,他自己就是一个锁,这个时候当然也是没有自动获取锁和自动释放锁的概念的。

因为没有同步锁,所以,我们就不能使用wait()和notify()方法

那么** 解决方法是:**

java5中提供了Lock机制的同时提供了处理Lock机制的通信控制的Condition接口

** 具体就是:**

1.使用Lock机制取代synchronized代码块和synchronized方法

2.使用Condition接口对象的await,signal,signalAll方法取代Object类中的wait,notify,notifyAll方法

** 解释一下:**

await()和wait()方法作用是相同相同的

signal()和notify()方法的作用是完全相同的

signalAll()和notifyAll()方法的作用是完全相同的

具体使用方法:

Condition本身是Lock的一个内部类,实例实质上被绑定到一个锁上,要为特定的锁Lock实例获得Condition实例,请使用newCondition()方法

生产者/消费者模型里面有两个类,所以我们要创建两个锁,分别用在不同的额

代码示例:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; /**
* 生产者消费者问题,涉及到几个类
* 第一,这个问题本身就是一个类,即主类
* 第二,既然是生产者、消费者,那么生产者类和消费者类就是必须的
* 第三,生产什么,消费什么,所以物品类是必须的,这里是馒头类
* 第四,既然是线程,那么就不是一对一的,也就是说不是生产一个消费一个,既然这样,多生产的往哪里放
* 现实中就是筐了,在计算机中也就是数据结构,筐在数据结构中最形象的就是栈了,因此还要一个栈类
*/ public class ProduceConsume {
public static void main(String[] args) {
SyncStack ss = new SyncStack();//建造一个装馒头的框
Producer p = new Producer(ss);//新建一个生产者,并把已经创建好框传给它,使其使用这个框
Consume c = new Consume(ss);//新建一个消费者,并把已经创建好框传给它,使其使用这个框
Thread tp = new Thread(p);//接口方法创建一个生产者线程
Thread tc = new Thread(c);//接口方法创建一个消费者线程
tp.start();//开启生产者线程
tc.start();//开启消费者线程
}
} //馒头类
class SteamBread{
int id;
SteamBread(int id){
this.id = id;
} @Override
public String toString() { //重写馒头返回的方法
return "SteamBread:" + id;
}
} //装馒头的框,栈结构
class SyncStack{
int index = 0;
SteamBread[] stb = new SteamBread[6]; //构造一个装馒头的数组,容量是6
final Lock lock = new ReentrantLock(); //创建一个锁
final Condition condition = lock.newCondition(); //为lock锁创建一个Conditon实例 //放入框中,相当于入栈
public void push(SteamBread sb){
lock.lock();//获取锁
try {
while (index==stb.length) { //筐满了,即栈满,
condition.await();//当前线程等待
}
condition.signal();//唤醒另外一个线程
stb[index]=sb;
this.index++;
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();//释放锁
} } //从框中拿出,相当于出栈
public SteamBread pop(){
lock.lock();//获取锁
try {
while (index==0) {//筐空了,即栈空
condition.await();//当前线程等待
}
this.index--;//push第n个之后,this.index++,使栈顶为n+1,故return之前要减一
condition.signal();//唤醒另外一个线程
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();//释放锁
}
return stb[index];
}
}

4.死锁

多线程通信的时候是非常容易造成死锁的,死锁是无法解决的,只能避免:

当A线程等待B线程所持有的锁,而B线程也在等在A线程所持有的锁时,这个收会发生死锁现象,对于死锁JVM是不检测的

由于死锁不会被检测出来,所以只能由程序员来保证线程不会导致死锁

最有名的死锁问题就是:哲学家吃面条的问题 **

避免死锁法则:当多个线程都要访问共享的资源A,B,C时,保证每一个线程都按照相同的顺序去访问他们,比如都都先访问A,接着B,最后C

** Thread类中一些过时的用法


suspend();使正在运行的线程放弃CPU,暂停运行

resume():是暂停的线程恢复运行

过时的用法是非常容易导致死锁的,所以不可再用

** 死锁情况:**

A线程获得对象锁,正在执行一个同步方法,如果B线程调用A线程的suspend()方法,此时A线程暂停运行,此时A线程放弃CPU,但是不会放弃暂用的锁,此时就造成了死锁

上一篇:MySQL中的insert ignore into, replace into用法总结


下一篇:sql练习记录