JAVA多线程——(三)多线程编程

JAVA多线程——(三)多线程编程

【一】Future

Future应用场景

  • 在并发编程中,我们经常用到非阻塞的模型,在之前的多线程的三种实现中,不管是继承thread类还是实现runnable接口,都无法保证获取到之前的执行结果。通过实现Callback接口,并用Future可以来接收多线程的执行结果。
  • Future表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加Callback以便在任务执行成功或失败后作出相应的操作。

FutureTask的类图结构

在这里插入图片描述

	public class Main {
    public static Integer consumerFunction(){
        System.out.println("这是消费者:");
        return 1;
    }

    public static Integer producerFunction(){
        System.out.println("这是生产者:");
        return 2;
    }


    public static void main(String args[]) throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();

        //生产者
        FutureTask<Integer> consumer = new FutureTask<Integer>(()->consumerFunction());
        //消费者
        FutureTask<Integer> producer = new FutureTask<Integer>(()->producerFunction());

        //提交任务
        executorService.submit(consumer);
        executorService.submit(producer);

        int resultProducer = producer.get();
        int resultConsumer = consumer.get();

        System.out.println("resultConsumer:"+resultConsumer+"  resultProducer:"+resultProducer);

    }
}

【二】CompletableFuture

CompletableFuture 是一个 Future
CompletableFuture 提供了 join() 方法,它的功能和 get() 方法是一样的,都是阻塞获取值,它们的区别在于 join() 抛出的是 unchecked Exception。

上面的代码确实没什么用,下面介绍几个 static 方法,它们使用任务来实例化一个 CompletableFuture 实例。

CompletableFuture.runAsync(Runnable runnable);
CompletableFuture.runAsync(Runnable runnable, Executor executor);
 
CompletableFuture.supplyAsync(Supplier<U> supplier);
CompletableFuture.supplyAsync(Supplier<U> supplier, Executor executor)
  • runAsync 方法接收的是 Runnable 的实例,意味着它没有返回值
  • supplyAsync 方法对应的是有返回值的情况

这两个方法的带 executor 的变种,表示让任务在指定的线程池中执行,不指定的话,通常任务是在 ForkJoinPool.commonPool() 线程池中执行的。

2.1 任务之间执行顺序(串行):

我们先来看执行两个任务的情况,首先执行任务 A,然后将任务 A 的结果传递给任务 B。

其实这里有很多种情况:
任务 A 是否有返回值
任务 B 是否需要任务 A 的返回值
任务 B 是否有返回值,等等。

有个明确的就是,肯定是任务 A 执行完后再执行任务 B。

我们用下面的 6 行代码来说:

CompletableFuture.runAsync(() -> {}).thenRun(() -> {}); 
CompletableFuture.runAsync(() -> {}).thenAccept(resultA -> {}); 
CompletableFuture.runAsync(() -> {}).thenApply(resultA -> "resultB");
 
 //任务 A 执行完执行 B,并且 B 不需要 A 的结果
CompletableFuture.supplyAsync(() -> "resultA").thenRun(() -> {});

//任务 A 执行完执行 B,B 需要 A 的结果,但是任务 B 不返回值
CompletableFuture.supplyAsync(() -> "resultA").thenAccept(resultA -> {});

//任务 A 执行完执行 B,B 需要 A 的结果,同时任务 B 有返回值
CompletableFuture.supplyAsync(() -> "resultA").thenApply(resultA -> resultA + " resultB");

前面 3 句代码:演示的是,任务 A 无返回值。第 2 行和第 3 行代码中,resultA 其实是 null。

第 4 句:用的是 thenRun(Runnable runnable),任务 A 执行完执行 B,并且 B 不需要 A 的结果。

第 5 句:用的是 thenAccept(Consumer action),任务 A 执行完执行 B,B 需要 A 的结果,但是任务 B 不返回值。

第 6 句:用的是 thenApply(Function fn),任务 A 执行完执行 B,B 需要 A 的结果,同时任务 B 有返回值。

这一小节说完了,如果任务 B 后面还有任务 C,往下继续调用 .thenXxx() 即可。

2.2 取两个任务的结果(并行)

我们来看怎么让任务 A 和任务 B 同时执行,然后取它们的结果进行后续操作,这里强调的是任务之间的并行工作。


如果使用 Future 的话,我们通常是这么写的:

ExecutorService executorService = Executors.newCachedThreadPool();
 
Future<String> futureA = executorService.submit(() -> "resultA");
Future<String> futureB = executorService.submit(() -> "resultB");
 
String resultA = futureA.get();
String resultB = futureB.get();

接下来,我们看看 CompletableFuture 中是怎么写的:
CompletableFuture<String> futureA = CompletableFuture.supplyAsync(() -> "resultA");
CompletableFuture<String> futureB = CompletableFuture.supplyAsync(() -> "resultB");

//使用两个任务的结果 resultA 和 resultB,thenAcceptBoth 表示后续的处理不需要返回值
futureA.thenAcceptBoth(futureB, (resultA, resultB) -> {});

//使用两个任务的结果 resultA 和 resultB,而 thenCombine 表示需要返回值。
futureA.thenCombine(futureB, (resultA, resultB) -> "result A + B");

//如果你不需要 resultA 和 resultB,runAfterBoth 方法。
futureA.runAfterBoth(futureB, () -> {});

第 4句代码:使用两个任务的结果 resultA 和 resultB,thenAcceptBoth 表示后续的处理不需要返回值

第 5 句代码:使用两个任务的结果 resultA 和 resultB,而 thenCombine 表示需要返回值。

第 6句代码:如果你不需要 resultA 和 resultB,runAfterBoth 方法。

2.3 取多个任务的结果(并行)

我们将介绍两个非常简单的静态方法:allOf() 和 anyOf() 方法:

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs){...}
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs) {...}
  • allOf :聚合了多个 CompletableFuture 实例,所以它是没有返回值的
CompletableFuture futureA = CompletableFuture.supplyAsync(() -> "resultA");
CompletableFuture futureB = CompletableFuture.supplyAsync(() -> 123);
CompletableFuture futureC = CompletableFuture.supplyAsync(() -> "resultC");

//并行执行三个任务,但是由于是多个任务,没有返回值
CompletableFuture<Void> future = CompletableFuture.allOf(futureA, futureB, futureC);
        
// 所以这里的 join() 将阻塞,直到所有的任务执行结束
future.join();
  • anyOf :就是只要有任意一个 CompletableFuture 实例执行完成就可以了
CompletableFuture futureA = CompletableFuture.supplyAsync(() -> "resultA");
CompletableFuture futureB = CompletableFuture.supplyAsync(() -> 123);
CompletableFuture futureC = CompletableFuture.supplyAsync(() -> "resultC");

//三个任务只要有一个有返回值了就行
CompletableFuture<Object> future = CompletableFuture.anyOf(futureA, futureB, futureC);

//最后一行的 join() 方法会返回最先完成的任务的结果,
//所以它的泛型用的是 Object,因为每个任务可能返回的类型不同
Object result = future.join();

2.4 异常处理

我们顺便来说下 CompletableFuture 的异常处理。这里我们要介绍两个方法:

public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn);
public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);

看下面的代码:

CompletableFuture.supplyAsync(() -> "resultA")
    .thenApply(resultA -> resultA + " resultB")
    .thenApply(resultB -> resultB + " resultC")
    .thenApply(resultC -> resultC + " resultD");

上面的代码中,任务 A、B、C、D 依次执行,如果任务 A 抛出异常(当然上面的代码不会抛出异常),那么后面的任务都得不到执行

方式一:
我们在任务 A 中抛出异常,并对其进行处理

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            throw new RuntimeException();
        })
                .exceptionally(ex -> "errorResultA")
                .thenApply(resultA -> resultA + " resultB")
                .thenApply(resultB -> resultB + " resultC")
                .thenApply(resultC -> resultC + " resultD");

        System.out.println(future.join());

方式二:
使用 handle(BiFunction fn) 来处理异常

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "resultA")
        .thenApply(resultA -> resultA + " resultB")
        // 任务 C 抛出异常
        .thenApply(resultB -> {throw new RuntimeException();})
        // 处理任务 C 的返回值或异常
        .handle(new BiFunction<Object, Throwable, Object>() {
            @Override
            public Object apply(Object re, Throwable throwable) {
                if (throwable != null) {
                    return "errorResultC";
                }
                return re;
            }
        })
        .thenApply(resultC -> resultC + " resultD");
 
System.out.println(future.join());

上面的代码使用了 handle 方法来处理任务 C 的执行结果,上面的代码中,re 和 throwable 必然有一个是 null,它们分别代表正常的执行结果和异常的情况。

当然,它们也可以都为 null,因为如果它作用的那个 CompletableFuture 实例没有返回值的时候,re 就是 null。

【三】Fock_Join

fork/join框架是ExecutorService接口的一个实现,可以帮助开发人员充分利用多核处理器的优势,编写出并行执行的程序,提高应用程序的性能;设计的目的是为了处理那些可以被递归拆分的任务。

fork/join框架与其它ExecutorService的实现类相似,会给线程池中的线程分发任务,不同之处在于它使用了工作窃取算法,所谓工作窃取,指的是对那些处理完自身任务的线程,会从其它线程窃取任务执行。

Fork/Join框架是Java 7提供的一个用于并行执行任务的框架,是一个把大任务分割成若干个小任务,最终汇总每个小任务结果后得到大任务结果的框架。简单来说,Fork就是把一个大任务切分为若干子任务并行的执行,Join就是合并这些子任务的执行结果,最后得到这个大任务的结果。比如计算1+2+…+10000,可以分割成10个子任务,每个子任务分别对1000个数进行求和,最终汇总这10个子任务的结果。

3.1 Fork/Join框架的工作原理

从上面Fork/Join框架的介绍中了解到,Fork/Join框架主要分为几个步骤实现。

1、分割任务。首先我们需要有一个fork类来把大任务分割成子任务,有可能子任务还是很大,所以还需要不停地分割,直到分割出的子任务足够小。

2、执行任务并合并结果。分割的子任务分别放在双端队列里,然后几个启动线程分别从双端队列里获取任务执行。子任务执行完的结果都统一放在一个队列里,启动一个线程从队列里拿数据,然后合并这些数据。

  • RecursiveAction:用于没有返回结果的任务,即执行任务后不会返回结果;
  • RecursiveTask:用于有返回结果的任务,即执行任务后会返回结果。
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
public class CountTask extends RecursiveTask<Integer> {
    // 阈值,表示拆分为几个子任务
	private static final int THRESHOLD = 2;
	private int start;
	private int end;
	public CountTask(int start, int end) {
		this.start = start;
		this.end = end;
	}
	@Override
	protected Integer compute() {
		int sum = 0;
		// 如果任务小于等于阈值则计算任务
		Boolean canCompute = (end - start) <= THRESHOLD;
		if (canCompute) {
			for (int i = start; i <= end; i++) {
				sum += i;
			}
		} else {
			// 如果任务大于阈值,就分裂成两个子任务计算
			int middle = (start + end) / 2;
			CountTask leftTask = new CountTask(start, middle);
			CountTask rightTask = new CountTask(middle + 1, end);
			// 执行子任务
			leftTask.fork();
			rightTask.fork();
			// 等待子任务执行完,并得到其结果
			int leftResult=leftTask.join();
			int rightResult=rightTask.join();
			// 合并子任务
			sum = leftResult + rightResult;
		}
		r
		eturn sum;
	}
	public static void main(String[] args) {
		ForkJoinPool forkJoinPool = new ForkJoinPool();
		// 生成一个计算任务,负责计算1+2+3+4
		CountTask task = new CountTask(1, 4);
		// 执行一个任务
		Future<Integer> result = forkJoinPool.submit(task);
		try {
			System.out.println(result.get());
		}
		catch (InterruptedException e) {
		}
		catch (ExecutionException e) {
		}
	}
}

【四】链接

https://blog.csdn.net/striveb/article/details/84140407
https://blog.csdn.net/w605283073/article/details/92418504

发布了130 篇原创文章 · 获赞 88 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/wenge1477/article/details/102996954