目录
并行设计模式属于设计优化的一部分,它是对一些常用的多线程结构的总结和抽象。与串行程序相比,并行程序的结构更加的复杂。因此合理的使用并行模式在多线程开发中更具有意义。在这里主要介绍Future、Master-Worker和生产者-消费者模型。
1.Future模式
Future模式有点类似于商品订单。比如在网购时,当我们看重某一件商品的时候,就可以提交订单,当订单处理完成之后,在家里等待商品送货上门即可。或者说更形象的我们发送Ajax请求的时候,页面是异步的进行后台处理,用户无须一直等待请求的结果,可以继续浏览或操作其他内容。该设计模式在JDK中有具体的实现,可以直接去使用。Future设计模式的实例代码如下:
首先定义一个获取数据的接口Data:
public interface Data {
String getRequest();
}
定义实际处理任务的类RealData
public class RealData implements Data {
private String result;
public RealData(String queryStr) {
System.out.println("根据" + queryStr + "进行查询,这是一个很耗时的操作..");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("操作完毕,获取结果");
result = "查询结果";
}
@Override
public String getRequest() {
return result;
}
}
定义FutureData,负责将RealData处理的结果装载返回:
public class FutureData implements Data{
private RealData realData ;
private boolean isReady = false;
public synchronized void setRealData(RealData realData) {
//如果已经装载完毕了,就直接返回
if(isReady){
return;
}
//如果没装载,进行装载真实对象
this.realData = realData;
isReady = true;
//进行通知
notify();
}
@Override
public synchronized String getRequest() {
//如果没装载好 程序就一直处于阻塞状态
while(!isReady){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//装载好直接获取数据即可
return this.realData.getRequest();
}
}
定义FutureClient类,暴露给用户去请求使用:
public class FutureClient {
public Data request(final String queryStr){
//1 我想要一个代理对象(Data接口的实现类)先返回给发送请求的客户端,告诉他请求已经接收到,可以做其他的事情
final FutureData futureData = new FutureData();
//2 启动一个新的线程,去加载真实的数据,传递给这个代理对象
new Thread(new Runnable() {
@Override
public void run() {
//3 这个新的线程可以去慢慢的加载真实对象,然后传递给代理对象
RealData realData = new RealData(queryStr);
futureData.setRealData(realData);
}
}).start();
return futureData;
}
}
测试类Main:
public class Main {
public static void main(String[] args) throws InterruptedException {
FutureClient fc = new FutureClient();
Data data = fc.request("请求参数");
System.out.println("请求发送成功!");
System.out.println("做其他的事情...");
String result = data.getRequest();
System.out.println(result);
}
}
执行的输出结果如下:
请求发送成功!
做其他的事情...
根据请求参数进行查询,这是一个很耗时的操作..
操作完毕,获取结果
查询结果
2.Master-Worker模式
Master-Worker模式是常用的并行计算模式。它的核心思想是系统由两类进程协作工作:Master进程和Worker进程。Master负责接收和分配任务,Worker负责处理子任务。当各个Worker子进程处理完成后,会将结果返回给Master,由Master做归纳和总结。其好处是能将一个大任务分解成若干个小任务,并行执行,从而提高系统的吞吐量。Master-Worker模式的实现代码如下:
定义Task类:
public class Task {
private int id;
private int price ;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
定义Worker类,负责处理Task任务:
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
public class Worker implements Runnable {
private ConcurrentLinkedQueue<Task> workQueue;
private ConcurrentHashMap<String, Object> resultMap;
public void setWorkQueue(ConcurrentLinkedQueue<Task> workQueue) {
this.workQueue = workQueue;
}
public void setResultMap(ConcurrentHashMap<String, Object> resultMap) {
this.resultMap = resultMap;
}
@Override
public void run() {
while (true) {
Task input = this.workQueue.poll();
if (input == null) break;
Object output = handle(input);
this.resultMap.put(Integer.toString(input.getId()), output);
}
}
private Object handle(Task input) {
Object output = null;
try {
//处理任务的耗时。。 比如说进行操作数据库。。。
Thread.sleep(500);
output = input.getPrice();
} catch (InterruptedException e) {
e.printStackTrace();
}
return output;
}
}
定义Master类,负责分发任务和对Worker处理的结果归并:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
public class Master {
//1 有一个盛放任务的容器
private ConcurrentLinkedQueue<Task> workQueue = new ConcurrentLinkedQueue<Task>();
//2 需要有一个盛放worker的集合
private HashMap<String, Thread> workers = new HashMap<String, Thread>();
//3 需要有一个盛放每一个worker执行任务的结果集合
private ConcurrentHashMap<String, Object> resultMap = new ConcurrentHashMap<String, Object>();
//4 构造方法
public Master(Worker worker, int workerCount) {
worker.setWorkQueue(this.workQueue);
worker.setResultMap(this.resultMap);
for (int i = 0; i < workerCount; i++) {
this.workers.put(Integer.toString(i), new Thread(worker));
}
}
//5 需要一个提交任务的方法
public void submit(Task task) {
this.workQueue.add(task);
}
//6 需要有一个执行的方法,启动所有的worker方法去执行任务
public void execute() {
for (Map.Entry<String, Thread> me : workers.entrySet()) {
me.getValue().start();
}
}
//7 判断是否运行结束的方法
public boolean isComplete() {
for (Map.Entry<String, Thread> me : workers.entrySet()) {
if (me.getValue().getState() != Thread.State.TERMINATED) {
return false;
}
}
return true;
}
//8 计算结果方法
public int getResult() {
int priceResult = 0;
for (Map.Entry<String, Object> me : resultMap.entrySet()) {
priceResult += (Integer) me.getValue();
}
return priceResult;
}
}
测试类Main.class
import java.util.Random;
public class Main {
public static void main(String[] args) {
Master master = new Master(new Worker(), 20);
Random r = new Random();
for (int i = 1; i <= 100; i++) {
Task t = new Task();
t.setId(i);
t.setPrice(r.nextInt(1000));
master.submit(t);
}
master.execute();
long start = System.currentTimeMillis();
while (true) {
if (master.isComplete()) {
long end = System.currentTimeMillis() - start;
int priceResult = master.getResult();
System.out.println("最终结果:" + priceResult + ", 执行时间:" + end);
break;
}
}
}
}
输出的结果为:
最终结果:50921, 执行时间:2537
3.生产者-消费者
生产者和消费者也是一个非常经典的多线程模式,我们在实际开发中应用非常广泛的思想理念。在生产-消费模式中,通常有两类线程,即若干个生产者的线程和若干个消费者的线程。生产者线程负责提交用户请求,消费者线程则负责具体处理生产者提交的任务,在生产者和消费者之间通过共享内存缓存区进行通信。
定义处理的数据Data:
public final class Data {
private String id;
private String name;
public Data(String id, String name){
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString(){
return "{id: " + id + ", name: " + name + "}";
}
}
定义Privider,负责生产任务:
import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class Provider implements Runnable{
//共享缓存区
private BlockingQueue<Data> queue;
//多线程间是否启动变量,有强制从主内存中刷新的功能。即时返回线程的状态
private volatile boolean isRunning = true;
//id生成器
private static AtomicInteger count = new AtomicInteger();
//随机对象
private static Random r = new Random();
public Provider(BlockingQueue queue){
this.queue = queue;
}
@Override
public void run() {
while(isRunning){
try {
//随机休眠0 - 1000 毫秒 表示获取数据(产生数据的耗时)
Thread.sleep(r.nextInt(1000));
//获取的数据进行累计...
int id = count.incrementAndGet();
//比如通过一个getData方法获取了
Data data = new Data(Integer.toString(id), "数据" + id);
System.out.println("当前线程:" + Thread.currentThread().getName() + ", 获取了数据,id为:" + id + ", 进行装载到公共缓冲区中...");
if(!this.queue.offer(data, 2, TimeUnit.SECONDS)){
System.out.println("提交缓冲区数据失败....");
//do something... 比如重新提交
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void stop(){
this.isRunning = false;
}
}
定义Consumer,负责取任务:
import java.util.Random;
import java.util.concurrent.BlockingQueue;
public class Consumer implements Runnable{
private BlockingQueue<Data> queue;
public Consumer(BlockingQueue queue){
this.queue = queue;
}
//随机对象
private static Random r = new Random();
@Override
public void run() {
while(true){
try {
//获取数据
Data data = this.queue.take();
//进行数据处理。休眠0 - 1000毫秒模拟耗时
Thread.sleep(r.nextInt(1000));
System.out.println("当前消费线程:" + Thread.currentThread().getName() + ", 消费成功,消费数据为id: " + data.getId());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试类Main
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
public class Main {
public static void main(String[] args) throws Exception {
//内存缓冲区
BlockingQueue<Data> queue = new LinkedBlockingQueue<Data>(10);
//生产者
Provider p1 = new Provider(queue);
Provider p2 = new Provider(queue);
Provider p3 = new Provider(queue);
//消费者
Consumer c1 = new Consumer(queue);
Consumer c2 = new Consumer(queue);
Consumer c3 = new Consumer(queue);
//创建线程池运行,这是一个缓存的线程池,可以创建无穷大的线程,没有任务的时候不创建线程。空闲线程存活时间为60s(默认值)
ExecutorService cachePool = Executors.newCachedThreadPool();
cachePool.execute(p1);
cachePool.execute(p2);
cachePool.execute(p3);
cachePool.execute(c1);
cachePool.execute(c2);
cachePool.execute(c3);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
p1.stop();
p2.stop();
p3.stop();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// cachePool.shutdown();
// cachePool.shutdownNow();
}
}
4.fork/join框架
假如表里有个一亿的商品,我们来统计一下商品个数。这里首先模拟一个数据库的操作,代码如下:
package com.martin.learn.concurrent.forkjoin;
import java.util.concurrent.ThreadLocalRandom;
/**
* @author: martin
* @date: 2020/2/21
*/
public class DBUtils {
private static final ThreadLocalRandom random = ThreadLocalRandom.current();
public static int getMin() {
return random.nextInt(10);
}
public static int getMax() {
return 100000000;
}
/**
* 模拟计算商品记录数
*
* @param startId 开始索引
* @param endId 结束索引
* @return
*/
public static int sumRecorders(int startId, int endId) {
try {
//模拟查询数据库的操作 耗时5ms
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return endId - startId;
}
}
4.1 单线程问题
假如我们使用单线程来统计商品的个数:
package com.martin.learn.concurrent.forkjoin;
/**
* @author: martin
* @date: 2020/2/21
*/
public class SingleThreadTest {
/**
* 通常数据库一次查询的记录数最多大概5000行,不要太多
*/
private final static int THRESHOLD = 5000;
private static void sum() {
int start = DBUtils.getMin();
int end = DBUtils.getMax();
int sum = 0;
long startTime = System.currentTimeMillis();
while (start < end) {
sum += DBUtils.sumRecorders(start, start + THRESHOLD);
start = start + THRESHOLD + 1;
}
long endTime = System.currentTimeMillis();
System.out.println("sum:" + sum + ",cost:" + (endTime - startTime) / 1000 + "s");
}
public static void main(String[] args) {
sum();
}
}
代码输出如下:
sum:99980000,cost:112s
执行统计完成大概112s,时间比较长,我们可以通过fork/join框架做一下优化。
4.2 fork-join
fork-join可以充分利用多核CPU的优势,把一个任务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行。fork-join的思想是分而治之,示意图如下:
fork-join框架Java执行的核心类是ForkJoinPool,实例代码如下:
package com.martin.learn.concurrent.forkjoin;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
/**
* @author: martin
* @date: 2020/2/21
*/
public class SumTask extends RecursiveTask<Long> {
private static final int THRESHOLD = 5000;
private int startId;
private int endId;
public SumTask(int startId, int endId) {
this.startId = startId;
this.endId = endId;
}
@Override
protected Long compute() {
if (endId - startId <= THRESHOLD) {
return Long.valueOf(DBUtils.sumRecorders(startId, endId));
} else {
int mid = (startId + endId) / 2;
SumTask leftTask = new SumTask(startId, mid);
SumTask rightTask = new SumTask(mid + 1, endId);
invokeAll(leftTask, rightTask);
long leftJoin = leftTask.join();
long rightJoin = rightTask.join();
return leftJoin + rightJoin;
}
}
public static void main(String[] args) {
//指定并行的数据
ForkJoinPool forkJoinPool = new ForkJoinPool(100);
long startTime = System.currentTimeMillis();
int start = DBUtils.getMin();
int end = DBUtils.getMax();
SumTask sumTask = new SumTask(start,end);
forkJoinPool.invoke(sumTask); //拆分计算,调用invoke,实际执行的是compute()
System.out.println("sum:" + sumTask.join() + ",cost:" + (System.currentTimeMillis() - startTime) / 1000 + "s");
}
}
运行结果如下:
sum:99967230,cost:2s
这里我们可以看到执行的时间大大地缩短。