JAVA的并发编程(六): 多线程的设计模式

本文深入讲解Future模式、Master-worker模式及生产者-消费者模式的原理与实现,通过具体示例展示如何利用Java语言进行高效并发编程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

一、Future模式

1)用Java实现Future模式

1.创建一个Data接口

2.代理数据FutureData 

3.真实数据RealData 

4.客户端Client向Service发起请求,网购餐具

5.Server向数据源Data慢慢请求真实数据,并返回给客户端

2)JDK自带的Callable、Future和FutureTask

1.Callable与Runnable

2.Future

3.FutureTask

4.使用示例

二、Master-worker模式

1)简介

2)代码实现

1.Main主程序(依据处理器数量限制开启的线程数)

2.Task任务

3.Worker工作类(基类)

4.Worker工作类(扩展类)

5.Master控制器

三、生产者-消费者模式

1)概述

2)实现

1.Data数据

2.Provider生产者

3.Cusumer消费者

4.Main主程序


一、Future模式

1)用Java实现Future模式

我们先用一个例子来对future模式进行形象的分析:

情景1:

假如你突然想做饭,但是没有厨具,也没有食材。

在往常,我们可能会先去菜场买菜,然后去超市购买厨具,最后回家烹饪,这是一个迭代的流程。

在厨具买回来前,你没办法进行烹饪,这显然相当浪费时间。面对这种尴尬的处境,程序员就会想:在主线程买菜的期间,能不能发起一个子线程去移步买菜?

Where there is a will,there is a way,future 模式应运而生!

情景2:

我们去超市买食材,在此期间我们当然不会闲着,用手机发起了一次网购,相当于在主线程里面另起一个子线程去网购厨具,或者说发起一个Ajax请求。我们无需一直在家里等待快递配送,而是可以继续买菜,在我们回家的时候,快递送来了厨具。

 

执行步骤:

1.创建一个Data接口

代理数据和真实数据都实现了这个接口并重写getRequest方法用来返回数据

public interface Data {
	String getRequest();
}

2.代理数据FutureData 

实现接口并封装了RealData ,如果没有装载好getRequest就处于阻塞状态,否则就正常执行

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();
	}
}

3.真实数据RealData 

实现接口,实例化时购买厨具,模拟耗时5秒

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;
	}
}

4.客户端Client向Service发起请求,网购餐具

public class Main {
	public static void main(String[] args) throws InterruptedException {		
		FutureService fc = new FutureService();
		Data data = fc.request("网购厨具");
		System.out.println("下单成功!");
		System.out.println("去菜场买菜...");		
		String result = data.getRequest();
		System.out.println(result);
	}
}

5.Server向数据源Data慢慢请求真实数据,并返回给客户端

public class FutureService {
	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;
	}	
}

以下是执行结果:

2)JDK自带的Callable、Future和FutureTask

 

1.Callable与Runnable

先说一下java.lang.Runnable吧,它是一个接口,在它里面只声明了一个run()方法:

public interface Runnable {
    public abstract void run();
}

由于run()方法返回值为void类型,所以在执行完任务之后无法返回任何结果。

Callable位于java.util.concurrent包下,它也是一个接口,在它里面也只声明了一个方法,只不过这个方法叫做call():

public interface Callable<V> {    
    V call() throws Exception;
}

 可以看到,这是一个泛型接口,call()函数返回的类型就是传递进来的V类型。

  那么怎么使用Callable呢?一般情况下是配合ExecutorService线程池来使用的,在ExecutorService接口中声明了若干个submit方法的重载版本:

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

  第一个submit方法里面的参数类型就是Callable。

  暂时只需要知道Callable一般是和ExecutorService配合来使用的,具体的使用方法讲在后面讲述。

  一般情况下我们使用第一个submit方法和第三个submit方法,第二个submit方法很少使用。

       submit方法和excute方法的区别:submit可以传入Callable接口,而且他有返回值

2.Future

Future就是对于具体的Runnable或者Callable任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过get方法获取执行结果,该方法会阻塞直到任务返回结果。

  Future类位于java.util.concurrent包下,它是一个接口:

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();

    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

cancel方法用来取消任务,如果取消任务成功则返回true,如果取消任务失败则返回false。参数mayInterruptIfRunning表示是否允许取消正在执行却没有执行完毕的任务,如果设置true,则表示可以取消正在执行过程中的任务。如果任务已经完成,则无论mayInterruptIfRunning为true还是false,此方法肯定返回false,即如果取消已经完成的任务会返回false;如果任务正在执行,若mayInterruptIfRunning设置为true,则返回true,若mayInterruptIfRunning设置为false,则返回false;如果任务还没有执行,则无论mayInterruptIfRunning为true还是false,肯定返回true。   在Future接口中声明了5个方法,下面依次解释每个方法的作用:

  • isCancelled方法表示任务是否被取消成功,如果在任务正常完成前被取消成功,则返回 true。
  • isDone方法表示任务是否已经完成,若任务完成,则返回true;
  • get()方法用来获取执行结果,这个方法会产生阻塞,会一直等到任务执行完毕才返回;
  • get(long timeout, TimeUnit unit)用来获取执行结果,如果在指定时间内,还没获取到结果,就直接返回null。

  也就是说Future提供了三种功能:

  1)判断任务是否完成;

  2)能够中断任务;

  3)能够获取任务执行结果。

  因为Future只是一个接口,所以是无法直接用来创建对象使用的,因此就有了下面的FutureTask。

3.FutureTask

我们先来看一下FutureTask的实现:

public class FutureTask<V> implements RunnableFuture<V>

   FutureTask类实现了RunnableFuture接口,我们看一下RunnableFuture接口的实现:

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

   可以看出RunnableFuture继承了Runnable接口和Future接口,而FutureTask实现了RunnableFuture接口。所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

  FutureTask提供了2个构造器:

public FutureTask(Callable<V> callable) {}

public FutureTask(Runnable runnable, V result) {}

  事实上,FutureTask是Future接口的一个唯一实现类。

4.使用示例

使用Callable+Future获取执行结果

  • 新建Task实现Callable接口
class Task implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("子线程在进行计算");
        Thread.sleep(3000);
        int sum = 0;
        for(int i=0;i<100;i++)
            sum += i;
        return sum;
    }
}
  • 调用submit方法生成Futuer对象,调用future.get() 阻塞直到获取真实数据;
public class Test {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        Task task = new Task();
        Future<Integer> future = executor.submit(task);
        executor.shutdown();
         
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }         
        System.out.println("主线程在执行任务");
         
        try {
            System.out.println("task运行结果"+future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("所有任务执行完毕");
    }
}

   执行结果:

  2.使用Callable+FutureTask获取执行结果

public class Test2 {
    public static void main(String[] args) {
        //第一种方式
        ExecutorService executor = Executors.newCachedThreadPool();
        Task2 task = new Task2();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        executor.submit(futureTask);
        executor.shutdown();

        //第二种方式,注意这种方式和第一种方式效果是类似的,只不过一个使用的是ExecutorService,一个使用的是Thread
        /*Task task = new Task();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        Thread thread = new Thread(futureTask);
        thread.start();*/
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }

        System.out.println("主线程在执行任务");
        
        try {
            System.out.println("task运行结果"+futureTask.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println("所有任务执行完毕");
    }
}

class Task2 implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("子线程在进行计算");
        Thread.sleep(3000);
        int sum = 0;
        for(int i=0;i<100;i++)
            sum += i;
        return sum;
    }
}

   如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。

二、Master-worker模式

1)简介

Master-Worker模式是常用的并行设计模式。它的核心思想是,系统有两个进程协议工作:Master进程和Worker进程。Master进程负责接收和分配任务,Worker进程负责处理子任务。当各个Worker进程将子任务处理完后,将结果返回给Master进程,由Master进行归纳和汇总,从而得到系统结果。处理过程如下图:

Master-Worker模式的好处是,它能将大任务分解成若干个小任务,并发执行,从而提高系统性能。而对于系统请求者Client来说,任务一旦提交,Master进程就会立刻分配任务并立即返回,并不会等系统处理完全部任务再返回,其处理过程是异步的。

Master-Worker模式的主要结构如下图:

 

 如上图所示

Master:主要进程,用于任务的分配和最终结果的合并,它维护着一个Worker进程队列、子任务队列和子结果集,Worker进程中的Worker进程不断的从任务队列中提取要处理的子任务,并将子任务的处理结果放入到子结果集中。这里使用上一个笔记JAVA的并发编程(五): 同步类容器和并发类容器中提到的并发类容器,ConcurrentLinkedQueue用于存放任务,HashMap存放Worker,ConcurrentHashMap存放结果集。

Worker:用于实际处理一个任务;客户端进程:用于启动系统,调度开启Master。

2)代码实现

1.Main主程序(依据处理器数量限制开启的线程数)

public class Main {
	public static void main(String[] args) {
		System.out.println("我的机器可用Processor数量:" + Runtime.getRuntime().availableProcessors());
		Master master = new Master(new MyWorker(), Runtime.getRuntime().availableProcessors()*25);
		Random r = new Random();
		//插入任务
		for(int i = 1; i<= 100; i++){
			Task t = new Task();
			t.setId(i);
			t.setName("任务"+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 ret = master.getResult();
				System.out.println("最终结果:" + ret + ", 执行耗时:" + end);
				break;
			}
		}
	}
}

2.Task任务

public class Task {
	private int id ;
	private String name;
	private int price;
	public int getId() { return id; }
	public void setId(int id) { this.id = id; }
	public String getName() { return name; }
	public void setName(String name) { this.name = name; }
	public int getPrice() { return price; }
	public void setPrice(int price) { this.price = price; }
}

3.Worker工作类(基类)

public class Worker implements Runnable {
	private ConcurrentLinkedQueue<Task> workQueue;
	private ConcurrentHashMap<String, Object> resultMap;
	public void setWorkerQueue(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);
		}
	}
	//可以通过重新handle方法扩展
	public Object handle(Task input){
		return null;
	}
}

4.Worker工作类(扩展类)

public class MyWorker extends Worker {
	@Override
	public  Object handle(Task input) {
		Object output = null;
		try {
			//表示处理task任务的耗时,可能是数据的加工,也可能是操作数据库...
			Thread.sleep(500);
			output = input.getPrice();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return output;
	}
}

5.Master控制器

public class Master {
	//1 应该有一个承装任务的集合
	private ConcurrentLinkedQueue<Task> workQueue = new ConcurrentLinkedQueue<Task>();
	//2 使用HashMap去承装所有的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对象都需要有Master的引用 workQueue用于任务的领取,resultMap用于任务的提交
		worker.setWorkerQueue(this.workQueue);
		worker.setResultMap(this.resultMap);
		for(int i = 0 ; i < workerCount; i++){
			//key表示每一个worker的名字, value表示线程执行对象
			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();
		}
	}
	//8 判断线程是否执行完毕
	public boolean isComplete() {
		for(Map.Entry<String, Thread> me : workers.entrySet()){
			if(me.getValue().getState() != Thread.State.TERMINATED){
				return false;
			}
		}		
		return true;
	}
	//9 返回结果集数据
	public int getResult() {
		int ret = 0;
		for(Map.Entry<String, Object> me : resultMap.entrySet()){
			//汇总的逻辑..
			ret += (Integer)me.getValue();
		}
		return ret;
	}
}

运行结果为:

三、生产者-消费者模式

1)概述

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。

这个阻塞队列就是用来给生产者和消费者解耦的。纵观大多数设计模式,都会找一个第三者出来进行解耦,如工厂模式的第三者是工厂类,模板模式的第三者是模板类。在学习一些设计模式的过程中,如果先找到这个模式的第三者,能帮助我们快速熟悉一个设计模式。

2)实现

1.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 + "}";
	}
}

2.Provider生产者

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;
	}
}

3.Cusumer消费者

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();
			}
		}
	}
}

4.Main主程序

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();
	}
}

执行结果为:

这是最简单的不包含线程通信的版本,其实可以加入线程通信,让生产者和消费者之间可以互相唤醒,具体详见之前的笔记:JAVA的并发编程(四): 线程的通信

 

 

PS:博文仅作为个人学习笔记,如有错误欢迎指正,转载请注明出处~

详见:笔记分类导航目录

参考文档:

1. Java并发编程:Callable、Future和FutureTask

2. 彻底理解Java的Future模式

3. 并行程序设计模式--Master-Worker模式

本书浅显易懂的介绍了JAVA线程相关的设计模式,通过程序范例和UML图示来一一解说,书中代码的重要部分加了标注以使读者更加容易理解,再加上图文并茂,对于初学者还是程序设计高手来说,这都是一本学习和认识JAVA设计模式的一本好书。(注意,本资源附带书中源代码可供参考) 多线程与并发处理是程序设计好坏优劣的重要课题,本书通过浅显易懂的文字与实例来介绍Java线程相关的设计模式概念,并且通过实际的Java程序范例和 UML图示来一一解说,书中在代码的重要部分加上标注使读者更加容易解读,再配合众多的说明图解,无论对于初学者还是程序设计高手来说,这都是一本学习和认识设计模式非常难得的好书。 书中包含Java线程的介绍导读、12个重要的线程设计模式和全书总结以及丰富的附录内容。第一章相关线程设计模式的介绍,都举一反三使读者学习更有效。最后附上练习问题,让读者可以温故而知新,能快速地吸收书中的精华,书中最后附上练习问题解答,方便读者学习验证。 目录 漫谈UML UML 类图 类和层次结构的关系 接口与实现 聚合 访问控制 类间的关联性 顺序图 处理流程和对象间的协调 时序图 Introduction 1 Java语言的线程 Java语言的线程 何谓线程 明为追踪处理流程,实则追踪线程 单线程程序 多线程程序 Thread类的run方法和start方法 线程的启动 线程的启动(1)——利用Thread类的子类 线程的启动(2)——利用Runnable接口 线程的暂时停止 线程的共享互斥 synchronized方法 synchronized阻挡 线程的协调 wait set——线程的休息室 wait方法——把线程放入wait set notify方法——从wait set拿出线程 notifyAll方法——从wait set拿出所有线程 wait、notify、notifyAll是Object类的方法 线程的状态移转 跟线程有关的其他话题 重点回顾 练习问题 Introduction 2 多线程程序的评量标准 多线程程序的评量标准 安全性——不损坏对象 生存性——进行必要的处理 复用性——可再利用类 性能——能快速、大量进行处理 评量标准的总结 重点回顾 练习问题 第1章 Single Threaded Execution——能通过这座桥的,只有一个人 第2章 Immutable——想破坏它也没办法 第3章 Guarded Suspension——要等到我准备好喔 第4章 Balking——不需要的话,就算了吧 第5章 Producer-Consumer——我来做,你来用 第6章 Read-Write Lock——大家想看就看吧,不过看的时候不能写喔 第7章 read-Per-Message——这个工作交给你了 第8章 Worker Thread——等到工作来,来了就工作 第9章 Future——先给您这张提货单 第10章 Two-Phase Termination——快把玩具收拾好,去睡觉吧 第11章 Thread-Specific Storage——每个线程的保管箱 第12章 Active Object——接受异步消息的主动对象 总结 多线程程序设计的模式语言 附录A 练习问题的解答 附录B Java的内存模型 附录C Java线程的优先级 附录D 线程相关的主要API 附录E 参考文献
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值