java8 并行编程教程——Threads 和 Executors

java8 并行编程教程——Threads 和 Executors

欢迎java8 并行编程的第一部分。本文通过简易的示例代码让你轻松理解java8 并行编程。这时关于java 并行API系列教程的第一部分。接下来的15分钟你学习通过线程、任务、执行服务实现并行编程。
并行编程API首先在java5中发布,后续每个新版本逐步增强。本文的主要概念与java8之前版本一致,但示例代码基于java8按充分使用lambda表达式和其他新特性。如果你不熟悉lambda表达式,可以查看之前的内容。

Threads 和 Runnables

所有现代操作系统都支持并行编程,主要通过线程和进程。进程
是程序的实例,各个进程彼此独立运行。如果你启动个java应用,操作系统生成一个进程,和其他程序并行运行。在进程里,可以利用线程并行执行代码,可以最大化使用多核CPU.

JAVA从JDK1.0开始就支持线程,在开始线程之前,首先准备线程要执行的代码,通常称之为任务,通过实现Runnable函数式接口,其定义了一个无参的方法run(),如下面示例所示:

Runnable task = () -> {
    String threadName = Thread.currentThread().getName();
    System.out.println("Hello " + threadName);
};

task.run();

Thread thread = new Thread(task);
thread.start();

System.out.println("Done!");

因为Runnable是函数式接口,我们使用java8lambda表达式打印当前线程的名称至控制台。我们首先在主线程中直接执行runnable 任务,然后开启一个新线程执行。
结果可能如下:

Hello main
Hello Thread-0
Done!

或也如下:

Hello main
Done!
Hello Thread-0

因为并行执行,我们不能预知子线程是否在打印’done’代码之前或之后。因为顺序不能确定,使得并行编程在大型应用中非常复杂。

线程可以休眠一段时间,这可以方便地模拟耗时的任务,看下面示例:

Runnable runnable = () -> {
    try {
        String name = Thread.currentThread().getName();
        System.out.println("Foo " + name);
        TimeUnit.SECONDS.sleep(1);
        System.out.println("Bar " + name);
    }
    catch (InterruptedException e) {
        e.printStackTrace();
    }
};

Thread thread = new Thread(runnable);
thread.start();

当运行上述代码,你将注意到,在第二个打印代码和第一个代码之间有1秒钟延迟。TimeUnit是非常有用单位工作时间枚举类型,也可以使用Thread.sleep(1000)代替。

使用Thread类可能代码冗长且容易出错。因此在2004年java5中正式引入并行编程API,位于java.util.concurrent包中,其中包括许多有用的操作并行编程的类。从此,每个java新版都增强并行API,java8也提供了新类和方法。

下面让我们深入了解并行API中最重要的部分之一,执行服务(executor services)。

Executor

并行API引入ExecutorService 作为替代直接使用Thread类更高层的封装。Executor一般通过管理线程池执行并行任务,因此我们无需手工创建新的线程。在线程池中的线程执行完任务后,可以重复使用,所以我们使用单个执行器服务运行应用全生命周期中尽可能多并行任务。
这时第一个使用executor的示例:

ExecutorService executor = Executors.newSingleThreadExecutor();
    executor.submit(() -> {
        String threadName = Thread.currentThread().getName();
        System.out.println("Hello " + threadName);
});

// => Hello pool-1-thread-1

Executors类提供便利的工厂方法,用于创建不同类型的executor 服务。本例中Executors创建仅一个线程的线程池。
当运行程序,执行结果与上面示例类似,但你注意到一个重要的差异:java进程没有结束,Executors 需要手动显示结束,否则始终运行监听新的任务。
ExecutorService提供了两个方法实现关闭任务:shutdown()等待当前正在运行的任务完成,而shutdownNow()中断所有运行
任务接着立刻关闭executor。

这时我比较喜欢的方式关闭executor:

try {
    System.out.println("attempt to shutdown executor");
    executor.shutdown();
    executor.awaitTermination(5, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
    System.err.println("tasks interrupted");
}
finally {
    if (!executor.isTerminated()) {
        System.err.println("cancel non-finished tasks");
}
executor.shutdownNow();
System.out.println("shutdown finished");
}

executor延时关闭,用于结束当前正在运行的任务。等待最大时间5秒之后,中断未完成的任务并关闭executor。

Callables 和 Futures

除了Runnable,executor也可以执行另一种名为Callable任务。Callable是函数式接口,除了有返回值,其他和runnable一样。

下面代码中lambda表达式定义一个callable,休眠1秒后返回一个整数。

Callable<Integer> task = () -> {
    try {
        TimeUnit.SECONDS.sleep(1);
        return 123;
    }
    catch (InterruptedException e) {
        throw new IllegalStateException("task interrupted", e);
    }
};

Callable 能像Runnable一样提交给executor服务执行,那么Callable的执行结果是什么呢?submit()方法没有等待任务执行完成,executor服务不能直接返回callable执行结果。而是executor返回一个特定的Future类型,通过Fucture在之后的某个时间点可以获取实际的结果。

ExecutorService executor = Executors.newFixedThreadPool(1);
Future<Integer> future = executor.submit(task);

System.out.println("future done? " + future.isDone());

Integer result = future.get();

System.out.println("future done? " + future.isDone());
System.out.print("result: " + result);

提交一个Callable任务给executor执行器之后,我们通过检查future.isDone()方法,判断是否执行完成。我很确信其没有完成,因为上面的任务中返回一个整数之前,休眠了1秒。

调用get()方法,阻塞当前线程等待直到在返回实际结果123之前Callable任务完成。最终future完成,我们可以在控制台看到结果。

future done? false
future done? true
result: 123

Future和底层executor服务是紧耦合的,记住,每个没有终止的future,你关闭executor,将抛出异常。

executor.shutdownNow();
future.get();

你可能主要到创建executor,与之前的示例稍微有点差异。我们使用newFixedThreadPool(1)方法创建一个的executor服务,支持一个线程的线程池。其等价与newSingleThreadExecutor(),但我们之后可以简单通过传入一个比1大的值,增加线程池大小。

超时

任何调用future.get()方法,将阻塞当前线程直达后台callable执行完成。最坏的情况callable一直运行,这样是你的应用查询没有响应,可以通过传入超时时间消除这种场景。

ExecutorService executor = Executors.newFixedThreadPool(1);

Future<Integer> future = executor.submit(() -> {
    try {
        TimeUnit.SECONDS.sleep(2);
        return 123;
    }
    catch (InterruptedException e) {
        throw new IllegalStateException("task interrupted", e);
    }
});

future.get(1, TimeUnit.SECONDS);

执行上述代码,结果抛出TimeoutExcepton异常。

Exception in thread "main" java.util.concurrent.TimeoutException
at java.util.concurrent.FutureTask.get(FutureTask.java:205)

你可能已经知道异常的原因了:我们设定最大等待时间为1秒,但callable实际执行时间为2秒。

InvokeAll

executor 支持通过invokeAll()方法一次提交多个callable任务批量执行。这个方法接受一个callable集合,并返回future集合。

ExecutorService executor = Executors.newWorkStealingPool();

List<Callable<String>> callables = Arrays.asList(
    () -> "task1",
    () -> "task2",
    () -> "task3");

executor.invokeAll(callables)
    .stream()
    .map(future -> {
    try {
        return future.get();
    }
    catch (Exception e) {
        throw new IllegalStateException(e);
    }
    })
    .forEach(System.out::println);

这个示例中我们使用java8函数式流,为了处理invokeAll方法执行的所有返回的future。我们首先map每个future的返回值,然后print每一个至控制台。

InvokeAny

另外一个批量提交callable任务的方法是invokeAny(),与invokeAll()方法稍微有点不同,返回第一个callable任务执行完成的结果。

为了测试这种行为,我们使用一个方法模拟不同执行时间的callable,该方法休眠一定时间后返回给定的执行结果。

Callable<String> callable(String result, long sleepSeconds) {
    return () -> {
        TimeUnit.SECONDS.sleep(sleepSeconds);
        return result;
    };
}

我们使用该方法创建一组callable任务,他们拥有不同额执行时间,1~3秒。然后提交这些callable至executor通过invokeAny()方法,他们返回最快执行完成的callable任务,本例中是task2。

ExecutorService executor = Executors.newWorkStealingPool();

List<Callable<String>> callables = Arrays.asList(
    callable("task1", 2),
    callable("task2", 1),
    callable("task3", 3));

String result = executor.invokeAny(callables);
System.out.println(result);

// => task2

上面示例通过newWorkSteaingPool()方法创建另一种类型的executor,其为java8提供的工厂方法,返回ForkJoinPool类型的执行器,与正常的executor稍微不同,代替给定一个固定大小的线程池,ForkJoinPool创建线程池大小默认为主机CPU的核数。java7中ForkJoinPool已经存在。

Scheduled Executors

我们已经学习如何一次性提交并运行任务给executor,为了周期性运行常规任务多次,可以使用预定线程池。

ScheduledExecutorService 能后调度任务周期运行或一定时间之后一次运行。下面代码演示调度任务3秒之后运行。

ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

Runnable task = () -> System.out.println("Scheduling: " + System.nanoTime());
ScheduledFuture<?> future = executor.schedule(task, 3, TimeUnit.SECONDS);

TimeUnit.MILLISECONDS.sleep(1337);

long remainingDelay = future.getDelay(TimeUnit.MILLISECONDS);
System.out.printf("Remaining Delay: %sms", remainingDelay);

调度任务生成类型类型的future——ScheduleFuture类,除了
Future类提供的方法,还提供getDelay()方法,其返回剩余时间,且并行执行。

为了调度任务周期性执行,executor提供两个方法,scheduleAtFixedRate()scheduleWithFixedDelay(),第一个方法能够使用固定的时间频率执行任务,如下面代码示例的每秒1次。

ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

Runnable task = () -> System.out.println("Scheduling: " + System.nanoTime());

int initialDelay = 0;
int period = 1;
executor.scheduleAtFixedRate(task, initialDelay, period, TimeUnit.SECONDS);

另外,该方法接受一个初始延迟参数,即第一次执行之前需等待的时间。需要注意的是scheduleAtFixedRate()方法没有考虑实际执行任务的时间,所以你指定1秒周期,但是任务需要2秒,那么线程执行则更快。这种情况下,应该考虑使用scheduleWithFixedDelay()方法代替。这个方法就如前面描述的方式吻合,不同之处是等待周期是任务结束和下一个任务开始的间隔。示例如下:

ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);

Runnable task = () -> {
    try {
        TimeUnit.SECONDS.sleep(2);
        System.out.println("Scheduling: " + System.nanoTime());
    }
    catch (InterruptedException e) {
        System.err.println("task interrupted");
    }
};

executor.scheduleWithFixedDelay(task, 0, 1, TimeUnit.SECONDS);

该示例调度任务使用固定时间间隔,即任务结束和下一个任务开始间的时间间隔。初始延时时间为0,执行任务所需时间为2秒。所以我们最终执行间隔时间为0s,3s,6s,9s……
所以如果你不能预测调度任务执行时间,使用scheduleWithFixedDelay()是非常方便的。

我希望这篇文章对你有帮助,有好的建议可以给我留言。

    原文作者:neweastsun
    原文地址: https://blog.csdn.net/neweastsun/article/details/54798807
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞