生产者和消费者这个关系是个经典的多线程案例。现在我们编写一个Demo来模拟生产者和消费者之间的关系。
假如有两个类,一个是数据生产者类DataProvider,另一个是数据消费者类DataConsumer,这两个类同时对数据类Data进行操作,生产者类负责生产数据,消费者类负责消费数据,下面是对这个过程的描述。
class DataProvider implements Runnable{
private Data data;
public DataProvider(Data data) {
this.data=data;
}
public void run() {
for(int i=0;i<=50;i++) {
if(i%2==0) {
data.setName("张三");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
data.setTag("---学生");
}else {
data.setName("李四");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
data.setTag("---老师");
}
}
}
}
class DataConsumer implements Runnable{
private Data data;
public DataConsumer(Data data) {
this.data=data;
}
public void run() {
for(int i=0;i<50;i++) {
try {
Thread.sleep(20);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(data.getName()+data.getTag());
}
}
}
class Data{
private String name;
private String tag;
public void setName(String name) {
this.name=name;
}
public void setTag(String tag) {
this.tag=tag;
}
public String getName() {
return this.name;
}
public String getTag() {
return this.tag;
}
}
public class TestDemo{
public static void main(String args[]) {
Data data=new Data();
new Thread(new DataProvider(data)).start();
new Thread(new DataConsumer(data)).start();
}
}
输出如下:
张三null
张三null
张三null
张三null
李四---学生
李四---学生
李四---学生
李四---学生
李四---学生
张三---老师
张三---老师
张三---老师
张三---老师
张三---老师
李四---学生
李四---学生
李四---学生
李四---学生
李四---学生
张三---老师
张三---老师
张三---老师
有输出可以发现问题:张三的值一会是空一回是老师学生,李四的值也发生了变化。这种操作不同步数据有偏差的问题是什么原因导致的呢?这是因为以前我们经常写的代码都是由主方法调用的,但是上面的这个代码却是由多个线程对我们的类进行操作,所以问题就产生了。
出现了上述问题我们第一个想到的就是使用synchronized关键字来解决。下面对上述代码进行修改。
class DataProvider implements Runnable{
private Data data;
public DataProvider(Data data) {
this.data=data;
}
public void run() {
for(int i=0;i<=50;i++) {
if(i%2==0) {
data.set("张三","是一个学生");
}else {
data.set("李四","是一个老师");
}
}
}
}
class DataConsumer implements Runnable{
private Data data;
public DataConsumer(Data data) {
this.data=data;
}
public void run() {
for(int i=0;i<50;i++) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
data.get();
}
}
}
class Data{
private String name;
private String tag;
private boolean flag=true;
public synchronized void set(String name,String tag) {
this.name=name;
this.tag=tag;
}
public synchronized void get() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(this.name+"----"+this.tag);
}
}
public class TestDemo{
public static void main(String args[]) {
Data data=new Data();
new Thread(new DataProvider(data)).start();
new Thread(new DataConsumer(data)).start();
}
}
输出结果如下:
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是一个学生
张三----是
修改之后数据之间不会乱了,但是这样会出现一个更加严重的问题,那就是数据重复更加严重了。synchronized能解决的只是一个同步问题,但是无法解决数据交替问题(注释两点内容:以上两个输出结果都是只截取了片段,没有完整截取。其次就是根据电脑的运行速度,输出的结果会有所不同)。
出现上述现象应该怎样来解决呢?实际上我们可以考虑使用Object类里面的wait()来让线程进行等待。当生产者线程没有执行完的时候,Data这个类的门牌上亮的是红灯,当生产者类生产完数据的时候Data这个类才亮绿灯,表示可以取走数据了。
Object类里面的wait()方法有两个wait()是死等,意思就说只要不唤醒就一直在哪等着,但是还有一个参数是long型的wait是活等,等够设置的时间就自动唤醒。既然有等待线程,那么就会有唤醒线程,唤醒线程主要由两个:notify和notifyAll两个,notify只是唤醒第一个等待的线程,而notifyAll则是唤醒所有等待的线程,至于一次性唤醒这么多到底谁先执行?谁的优先级高谁先执行。
下面来看怎么使用java等待唤醒机制来解决上面出现的问题
class DataProvider implements Runnable{
private Data data;
public DataProvider(Data data) {
this.data=data;
}
public void run() {
for(int i=0;i<=50;i++) {
if(i%2==0) {
this.data.set("张三","是一个学生");
}else {
this.data.set("李四","是一个老师");
}
}
}
}
class DataConsumer implements Runnable{
private Data data;
public DataConsumer(Data data) {
this.data=data;
}
public void run() {
for(int i=0;i<50;i++) {
this.data.get();
}
}
}
class Data{
private String name;
private String tag;
//flag=true表示允许生产但是不允许消费者取走
//flag=false表示允许取走但是不允许生产者生产
private boolean flag=false;
public synchronized void set(String name,String tag) {
if(flag==true) {
try {
super.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
this.name=name;
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
this.tag=tag;
this.flag=true;
super.notify();
}
public synchronized void get() {
if(flag==false) {//正在生产,不能取走
try {
super.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(this.name+"----"+this.tag);
this.flag=false;//能够取走,此时不能再生产了
super.notify();//唤醒有可能正在等待的其他线程
}
}
public class TestDemo{
public static void main(String args[]) {
Data data=new Data();
new Thread(new DataProvider(data)).start();
new Thread(new DataConsumer(data)).start();
}
}
输出结果:
张三----是一个学生
李四----是一个老师
张三----是一个学生
李四----是一个老师
张三----是一个学生
李四----是一个老师
张三----是一个学生
李四----是一个老师
张三----是一个学生
李四----是一个老师
张三----是一个学生
李四----是一个老师
张三----是一个学生
李四-
如结果,上面出现的问题得到了很好的解决。这就是线程休眠和唤醒的详细解释。
下面是彩蛋:
sleep()和wait()的区别:
sleep是Thread类里面定义的方法,到了一定时间可以自动唤醒。
wait是Object类定义的方法,如果要想唤醒必须使用notify方法或者notifyAll方法才能唤醒