完整代码已上传至Github, 欢迎查看!
多线程,顾名思义,就是多个线程。
线程:存在在进程中,一个进程就相当于一个应用。而进程下有许多线程在运行,执行不同的任务。当然线程下还有纤程。
再来说说我对并发和并行的理解。并发,就是在一段时间,有好几个程序都被执行完,在该段时间内,这些程序就可以说是并发。但是随着科技发展,现在cpu都是好几个一起运行的,那么线程的运行就可能不是并发的,而是并行的,在同一时间,当一个线程在被一个cpu运行时,另一个cpu也可能在执行任务,因此叫并行。
下面进入正题,来说说生产者消费者模式。利用多线程,创建三个生产者和三个消费者,生产者负责生产数据并存放在公共数据域,直到数据总数大于指定数才停止, 而消费者负责消耗公共数据域的数据, 只要数据不为0,就一直消耗。
如上图,有三个生产者在不同线程中运行。三个线程都会竞争cpu,竞争成功的执行生产数据去产生数据。只要当前数据数不大于最大数据数,就一直生产。而三个消费者在三个不同的线程中,竞争cpu,成功的即可执行消费数据消费一个数据,只要当前数据数不为0, 就可以一直消耗。
但是有一个问题,假设最大数据数是10, 而当前数据数是9, 现在一号生产者竞争到cpu, 准备执行生产数据方法,但此时时间片段到,被迫停止运行, 等待下一次竞争cpu。而在这个过程中,二号生产者竞争到cpu, 执行完生产数据方法,当前数据数已经达到最大数据数,此时一号生产者竞争到cpu,执行完上次因时间没执行完的生产数据方法,完蛋:数据数现在变成11了。
这就是多线程编程必须要考虑的事情了,多线程的安全问题。很明显,刚刚出现问题就是因为三个生产者不知道彼此是否在运行生产数据这个方法,还有就是时间片段的问题,导致当前数据数超过了最大数据数。那么该如何避免此类问题呢?
synchronized
这个关键字拯救了我们。使用这个关键字修饰生产数据这个方法,就可以有效的避免这个问题。或者将要执行的代码放到synchronized块中,即synchronized{
此处放代码
}
那么问题来了,这个关键字到底是嘛意思呢,网上说法五花八门,在此我举个有趣的例子来说一下我的见解:
比如有一个公共厕所,很多人排队在等待上厕所,当你在进去之前,你会先敲敲门看看里边有没有人,如果有人就继续在外边等着,或者放弃上厕所;如果没有人,急急忙忙进去,记住先锁门,锁好门之后再干活,如果外边有人来了,敲门的话门锁着,他就只能等着,等你干完活之后把门打开出来了,别人才能进去。
这个关键字正是这个意思。他会对被修饰的方法加锁,如果有线程来访问这个方法,首先查询有没有线程正在访问这个方法,如果有,就让来访的线程暂做等待,如果没有,就放该线程进去访问,同时加锁,访问过程中不允许别的线程进入访问。
了解了这些知识后,我们来写生产者消费者模式就变得很简单了:
先实现公共数据域,在其中有最大数据数和当前数据数,并提供给生产者和消费者进行生产和消费数据的方法(注:两方法应该加锁,否则会出现上述情况)
package com.yc.producerConsumer.Synchronized;
/*
*@author yc
* @time 2018/11/16
*/
public class Resource {
private static int count = 0;
private static int size = 10;
public synchronized void produce() {
if (count < size) {
count++;
System.out.println("【" + Thread.currentThread().getName()
+ "】生产了一个资源,当前拥有" + count + "个资源!");
notifyAll();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consume() {
if (count > 0) {
count--;
System.out.println("【" + Thread.currentThread().getName() +
"】消费了一个资源, 当前拥有" + count + "个资源!");
notifyAll();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
然后编写生产者和消费者,两者都应该在线程中,时时刻刻准备调用priduce()和consume()生产或消费数据:
生产者:
package com.yc.producerConsumer.Synchronized;
import java.util.concurrent.TimeUnit;
/*
*@author yc
* @time 2018/11/16
*/
public class Producer implements Runnable{
private Resource resouse;
public Producer(Resource resouse, String ThreadName) {
this.resouse = resouse;
new Thread(this, ThreadName).start();
}
@Override
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
resouse.produce();
}
}
}
消费者:
package com.yc.producerConsumer.Synchronized;
import java.util.concurrent.TimeUnit;
/*
*@author yc
* @time 2018/11/16
*/
public class Consumer implements Runnable{
private Resource resouse;
public Consumer(Resource resouse, String ThreadName) {
this.resouse = resouse;
new Thread(this, ThreadName).start();
}
@Override
public void run() {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
resouse.consume();
}
}
}
下面就编写一个Test来测试一下:
package com.yc.producerConsumer.Synchronized;
/*
*@author yc
* @time 2018/11/16
*/
public class Test {
public static void main(String[] args) {
Resource resouse = new Resource();
new Producer(resouse, "一号生产者");
new Producer(resouse, "二号生产者");
new Producer(resouse, "三号生产者");
new Consumer(resouse, "一号消费者");
new Consumer(resouse, "二号消费者");
new Consumer(resouse, "三号消费者");
}
}
三个生产者三个消费者,产生的数据如下:
执行结果显示,运行结果是随机的,谁竞争到cpu谁就运行。但加了synchronized的方法,同一时间只有一个线程能进去执行。而其他线程在外边排队,由此可能会使整体的工作效率降低,如果synchronzied中的方法执行复杂的话,可能会有很多线程被挡在外边等待,因此需慎用!
多线程有多线程的好处,但在运行时一定要注意线程安全。
还有两种加锁的方法实现生产者消费者模式,代码均已上传至个人github,有兴趣的朋友可前往查看,链接见文首。