多线程中一般就两大类模型:一个是卖票模型,一个是生产者消费者模型。生产者生产数据,消费者取走数据就是消费者模型。具体来说是生产者生产数据,将数据放入一个公共信息存储点,然后消费者再从公共信息点取走数据。如下图:
下面根据图示给出实例:
class Message { // 公共信息类
private String title;
private String info;
public void set(String titile, String info) {
this.title = titile;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.info = info;
}
public void get() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.title + " is " + this.info);
}
}
class Producer implements Runnable { // 生产者类
private Message message;
public Producer(Message message) {
this.message = message;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
if (i % 2 == 0) {
this.message.set("java", "programming language");
} else {
this.message.set("javascript", "scripting language");
}
}
}
}
class Consumer implements Runnable { // 消费者类
private Message message;
public Consumer(Message message) {
this.message = message;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
this.message.get();
}
}
}
public class ProAndConModel {
public static void main(String[] args) {
Message message = new Message();
Producer pro = new Producer(message);
Consumer con = new Consumer(message);
new Thread(pro).start();
new Thread(con).start();
}
}
同时执行生产者和消费者两个线程,生产者set就是在产生数据,这里使用取模的方式让产生的数据发生变动。消费者get就是获取数据,为了让问题出现的更明显,在set中对数据的产生中间点加入sleep。截取运行结果中的一部分:
java is scripting language
javascript is programming language
可以发现数据错位。运行结果是消费者打印出来的,那么就是消费者接受的数据发生了错误。稍加分析便得知,生产者在没生产完毕的时候,消费者就将数据取走了,这时的问题本质还是同步问题。解决同步问题就需要加锁,问题的本源在于公共信息类的方法,需要确保在set的时候不能get。
public synchronized void set(String titile, String info) {
this.title = titile;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.info = info;
}
public synchronized void get() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.title + " is " + this.info);
}
给方法加上synchronized关键字。现在的执行结果:
javascript is scripting language
javascript is scripting language
javascript is scripting language...
会发现数据重复。线程被锁定保证了不会数据错位,但是执行过快导致了数据大量重复。解决这个问题就需要使用多线程的等待唤醒机制。
梳理一下整个模型的工作过程:生产者先生产数据,这个时候消费者是不可以取出数据的;生产数据完毕,消费者可以取出数据,这个时候生产者是不可以生产数据的;消费者取出数据完毕,生产者可以生产数据...所以可以看出这中间需要有一个标志位来限制生产者和消费者不同时进行操作。
比如有一个灯,红绿两色表示不同操作:开始是绿灯,这时生产者生产数据,而消费者不可以取走数据;生产者生产数据完毕,灯变成红色,这时生产者不能再生产数据,消费者可以取走数据,灯变成绿色...更新代码:
class Message {
private String title;
private String info;
// flag = true 表示可以生产,但是不可以消费
// flag = false 表示可以消费,但是不可以生产
private boolean flag = true;
public synchronized void set(String titile, String info) {
if(this.flag == false) {
try {
super.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.title = titile;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.info = info;
this.flag = false; // 生产过了
super.notify(); // 唤醒其他等待线程
}
public synchronized void get() {
if(this.flag == true) {
try {
super.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.title + " is " + this.info);
this.flag = true; // 恢复生产
super.notify(); // 唤醒等待线程
}
}
class Producer implements Runnable {
private Message message;
public Producer(Message message) {
this.message = message;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
if (i % 2 == 0) {
this.message.set("java", "programming language");
} else {
this.message.set("javascript", "scripting language");
}
}
}
}
class Consumer implements Runnable {
private Message message;
public Consumer(Message message) {
this.message = message;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
this.message.get();
}
}
}
public class ProAndConModel {
public static void main(String[] args) {
Message message = new Message();
Producer pro = new Producer(message);
Consumer con = new Consumer(message);
new Thread(pro).start();
new Thread(con).start();
}
}
需要注意的是wait()和notify()方法,两个方法都是继承于Object类的,并且存在方法重载(这里建议去查看源码认识其他重载的方法)。在哪个线程中使用wait方法,谁就由运行状态转为阻塞状态。比如一开始生产者生产数据,消费者也执行get(),但是在标志位的引导下执行wait()方法,那么消费者这个线程就变为阻塞状态,等生产者生产完毕执行notify(),就将消费者线程唤醒了,此时继续执行消费者线程。