并发编程-代码实现

一、并行流

Java 8 流的一个显著优点是,在某些情况下,它们可以很容易地并行化。这来自库的仔细设计,特别是流使用内部迭代的方式 - 也就是说,它们控制着自己的迭代器,这是一种特殊的迭代器,称为 Spliterator,它被限制为易于自动分割。使用.parallel()会产生魔法般的结果-流中的所有内容都作为一组并行任务运行。如果你的代码是使用 Streams 编写的,那并行化以提高速度似乎是一种琐事。

例如,考虑来自 Streams 的 Prime.java。查找质数可能是一个耗时的过程,我们可以看到该程序的计时:

// concurrent/ParallelPrime.java
import java.util.*;
import java.util.stream.*;
import static java.util.stream.LongStream.*;
import java.io.*;
import java.nio.file.*;

public class ParallelPrime {
    static final int COUNT = 100000;

    public static boolean isPrime(long n){
        return rangeClosed(2, (long)Math.sqrt(n)).noneMatch(i -> n % i == 0);
    }

    public static void main(String[] args) throws IOException {
        long startMili=System.currentTimeMillis();
        List<String> primes =
                iterate(2, i -> i + 1)
                        .parallel()              
                        .filter(ParallelPrime::isPrime)
                        .limit(COUNT)
                        .mapToObj(Long::toString)
                        .collect(Collectors.toList());
        long endMili=System.currentTimeMillis();
        System.out.println("并行的执行时间为:"+(endMili-startMili));

        startMili=System.currentTimeMillis();
        primes =
                iterate(2, i -> i + 1)
                        .filter(ParallelPrime::isPrime)
                        .limit(COUNT)
                        .mapToObj(Long::toString)
                        .collect(Collectors.toList());
        endMili=System.currentTimeMillis();
        System.out.println("执行时间为:"+(endMili-startMili));

    }
}

输出结果:

并行的执行时间为:679
执行时间为:1613

可以看到19行的.parallel()被注释掉后,运行时间大概是未被注释时的三倍。

虽然我们可以将编程问题转换为流,然后插入 parallel() 以加快速度。但实际上,仅在一部分情况下这很容易,在大多数情况下,使用.parallel()存在着许多陷阱。

二、创建和运行任务

如果无法通过并行流实现并发,则必须创建并运行自己的任务。运行任务的理想 Java 8 方法是 CompletableFuture,但在这里我们将使用更基本的工具介绍概念。

Java 并发的历史始于非常原始和有问题的机制,并且充满了各种尝试的改进。这些主要归入附录:低级并发 (Appendix: Low-Level Concurrency)。在这里,我们将展示一个规范形式,表示创建和运行任务的最简单,最好的方法。与并发中的所有内容一样,存在着各种变体,但这些变体要么降级到该附录,要么超出本书的范围。

2.1 创建线程并执行

在 Java 的早期版本中,是通过直接创建自己的Thread对象来使用线程的,并且需要手动调用构造函数并自己启动线程。但是现在不鼓励采用手动操作方法,在 Java 5 中,添加了类来帮助处理线程池。你可以将任务创建为单独的类(比如实现Runnable接口),然后将其交给 ExecutorService(即线程池) 以运行该类中定义的任务,ExecutorService 为我们管理线程。

下面是一个代码例子,首先,我们将创建一个几乎不执行任务的任务(通过实现Runnable接口)。它“sleep”(暂停执行)100 毫秒,显示其标识符和正在执行任务的线程的名称,然后完成:

// concurrent/NapTask.java
import onjava.Nap;
public class NapTask implements Runnable {
    final int id;
    public NapTask(int id) {
        this.id = id;
    }
    @Override
    public void run() {
        new Nap(0.1);// Seconds
        System.out.println(this + " " +
                Thread.currentThread().getName());
    }
    @Override
    public String toString() {
        return"NapTask[" + id + "]";
    }
}

其中调用的onjava.Nap实现如下:

// onjava/Nap.java
package onjava;
import java.util.concurrent.*;
public class Nap {
    public Nap(double t) { // Seconds
        try {
            TimeUnit.MILLISECONDS.sleep((int)(1000 * t));
        } catch(InterruptedException e){
            throw new RuntimeException(e);
        }
    }
    public Nap(double t, String msg) {
        this(t);
        System.out.println(msg);
    }
}

对 TimeUnit.MILLISECONDS.sleep() 的调用获取“当前线程”并在参数中将其置于休眠状态,这意味着该线程被挂起。但并不意味着底层处理器停止工作,操作系统将切换到其他任务。OS 任务管理器定期检查 sleep() 是否超时。当sleep结束时,线程被“唤醒”并给予处理时间。

你可以看到 sleep() 抛出一个受检的 InterruptedException ;这是原始 Java 设计中的一个部分,它通过突然断开它们来终止任务。因为它往往会产生不稳定的状态,所以后来不鼓励终止。但是,我们必须在需要或仍然发生终止的情况下捕获异常。

要执行任务,我们将从最简单的方法–SingleThreadExecutor 开始:

//concurrent/SingleThreadExecutor.java
import java.util.concurrent.*;
import java.util.stream.*;
import onjava.*;
public class SingleThreadExecutor {
    public static void main(String[] args) {
        ExecutorService exec =
            Executors.newSingleThreadExecutor();
        IntStream.range(0, 10)
            .mapToObj(NapTask::new)
            .forEach(exec::execute);
        System.out.println("All tasks submitted");
        exec.shutdown();
        while(!exec.isTerminated()) {
            System.out.println(
            Thread.currentThread().getName()+
            " awaiting termination");
            new Nap(0.1);
        }
    }
}

输出结果:

All tasks submitted
main awaiting termination
NapTask[0] pool-1-thread-1
main awaiting termination
main awaiting termination
NapTask[1] pool-1-thread-1
NapTask[2] pool-1-thread-1
main awaiting termination
main awaiting termination
NapTask[3] pool-1-thread-1
main awaiting termination
NapTask[4] pool-1-thread-1
main awaiting termination
NapTask[5] pool-1-thread-1
main awaiting termination
NapTask[6] pool-1-thread-1
main awaiting termination
NapTask[7] pool-1-thread-1
main awaiting termination
NapTask[8] pool-1-thread-1
main awaiting termination
NapTask[9] pool-1-thread-1

首先请注意,没有 SingleThreadExecutor 类。newSingleThreadExecutor() 是 Executors 中的一个工厂方法,它创建特定类型的 ExecutorService。

我创建了十个 NapTasks 并将它们提交给 ExecutorService,这意味着它们开始自己运行。然而,在此期间,main() 也在继续执行。当我运行 callexec.shutdown() 时,它告诉 ExecutorService 完成已经提交的任务,但不接受任何新任务。此时,这些任务仍然在运行,因此我们必须等到它们在退出 main() 之前完成。这是通过检查 exec.isTerminated() 来实现的,这在所有任务完成后变为 true。

请注意,main() 中线程的名称是 main,并且只有一个其他线程 pool-1-thread-1。此外,交错输出显示两个线程确实同时运行。

如果你只是调用 exec.shutdown(),程序将完成所有任务。也就是说,不需要 while(!exec.isTerminated()) :

// concurrent/SingleThreadExecutor2.java
import java.util.concurrent.*;
import java.util.stream.*;
public class SingleThreadExecutor2 {
    public static void main(String[] args)throws InterruptedException {
        ExecutorService exec
        =Executors.newSingleThreadExecutor();
        IntStream.range(0, 10)
            .mapToObj(NapTask::new)
            .forEach(exec::execute);
        exec.shutdown();
    }
}

输出结果:

NapTask[0] pool-1-thread-1
NapTask[1] pool-1-thread-1
NapTask[2] pool-1-thread-1
NapTask[3] pool-1-thread-1
NapTask[4] pool-1-thread-1
NapTask[5] pool-1-thread-1
NapTask[6] pool-1-thread-1
NapTask[7] pool-1-thread-1
NapTask[8] pool-1-thread-1
NapTask[9] pool-1-thread-1

当我们执行了callexec.shutdown()后,尝试提交新任务将抛出RejectedExecutionException:

// concurrent/MoreTasksAfterShutdown.java
import java.util.concurrent.*;
public class MoreTasksAfterShutdown {
    public static void main(String[] args) {
        ExecutorService exec
                =Executors.newSingleThreadExecutor();
        exec.execute(new NapTask(1));
        exec.shutdown();
        try {
            exec.execute(new NapTask(99));
        } catch(RejectedExecutionException e) {
            System.out.println(e);
        }
    }
}

输出结果:

java.util.concurrent.RejectedExecutionException: Task NapTask[99] rejected from java.util.concurrent.ThreadPoolExecutor@1540e19d[Shutting down, pool size = 1, active threads = 1, queued tasks = 0, completed tasks = 0]
NapTask[1] pool-1-thread-1

exec.shutdown()的替代方法是 exec.shutdownNow() ,它除了不接受新任务外,还会尝试通过中断任务来停止任何当前正在运行的任务。同样,中断是错误的,容易出错并且不鼓励。

2.2 使用更多线程

使用线程的重点是(几乎总是)更快地完成任务,那么我们为什么要限制自己使用 SingleThreadExecutor 呢?查看执行 Executors 的 Javadoc,你将看到更多选项。例如 CachedThreadPool:

// concurrent/CachedThreadPool.java
import java.util.concurrent.*;
import java.util.stream.*;
public class CachedThreadPool {
    public static void main(String[] args) {
        ExecutorService exec
        =Executors.newCachedThreadPool();
        IntStream.range(0, 10)
        .mapToObj(NapTask::new)
        .forEach(exec::execute);
        exec.shutdown();
    }
}

输出:

NapTask[4] pool-1-thread-5
NapTask[7] pool-1-thread-8
NapTask[9] pool-1-thread-10
NapTask[3] pool-1-thread-4
NapTask[6] pool-1-thread-7
NapTask[5] pool-1-thread-6
NapTask[2] pool-1-thread-3
NapTask[8] pool-1-thread-9
NapTask[1] pool-1-thread-2
NapTask[0] pool-1-thread-1

当你运行这个程序时,你会发现它完成得更快。这是有道理的,每个任务都有自己的线程,所以它们都并行运行,而不是使用相同的线程来顺序运行每个任务。这似乎没毛病,很难理解为什么有人会使用 SingleThreadExecutor。

要理解这个问题,我们需要一个更复杂的任务:

// concurrent/InterferingTask.java
public class InterferingTask implements Runnable {
    final int id;
    private static Integer val = 0;
    public InterferingTask(int id) {
        this.id = id;
    }
    @Override
    public void run() {
        for(int i = 0; i < 100; i++)
        val++;
    System.out.println(id + " "+
        Thread.currentThread().getName() + " " + val);
    }
}

每个任务都增加val一百次,所以我们最后得到的结果应该是1000,接下来我们使用CachedThreadPool来验证下我们的结果:

// concurrent/CachedThreadPool2.java
import java.util.concurrent.*;
import java.util.stream.*;
public class CachedThreadPool2 {
    public static void main(String[] args) {
    	ExecutorService exec=Executors.newCachedThreadPool();
    	IntStream.range(0, 10).mapToObj(InterferingTask::new).forEach(exec::execute);
    	exec.shutdown();
    }
}

运行结果如下:

2 pool-1-thread-3 211
1 pool-1-thread-2 311
3 pool-1-thread-4 311
0 pool-1-thread-1 132
4 pool-1-thread-5 411
6 pool-1-thread-7 511
7 pool-1-thread-8 645
5 pool-1-thread-6 675
8 pool-1-thread-9 775
9 pool-1-thread-10 875

可以看到,输出并不是我们所期望的,并且如果我们再次运行,得到的结果和上次还不一样:

0 pool-1-thread-1 100
1 pool-1-thread-2 300
2 pool-1-thread-3 200
3 pool-1-thread-4 400
4 pool-1-thread-5 506
5 pool-1-thread-6 585
8 pool-1-thread-9 685
6 pool-1-thread-7 785
7 pool-1-thread-8 885
9 pool-1-thread-3 985

这就是多线程带来的问题,我们称上面的例子为线程不安全。将CachedThreadPool换成SingleThreadExecutor,再观察下会发生什么:

// concurrent/SingleThreadExecutor3.java
import java.util.concurrent.*;
import java.util.stream.*;
public class SingleThreadExecutor3 {
    public static void main(String[] args)throws InterruptedException {
        ExecutorService exec
                =Executors.newSingleThreadExecutor();
        IntStream.range(0, 10)
                .mapToObj(InterferingTask::new)
                .forEach(exec::execute);
        exec.shutdown();
    }
}

不论执行几次,最后得到的结果都如下所示:

0 pool-1-thread-1 100
1 pool-1-thread-1 200
2 pool-1-thread-1 300
3 pool-1-thread-1 400
4 pool-1-thread-1 500
5 pool-1-thread-1 600
6 pool-1-thread-1 700
7 pool-1-thread-1 800
8 pool-1-thread-1 900
9 pool-1-thread-1 1000

这是 SingleThreadExecutor 的主要好处 - 因为它一次运行一个任务,这些任务不会相互干扰,因此强加了线程安全性。这种现象称为线程封闭,因为在单线程上运行任务限制了它们的影响。线程封闭限制了加速,但可以节省很多困难的调试和重写。

2.3 产生结果

因为 InterferingTask 是一个 Runnable ,它没有返回值,因此只能使用副作用产生结果 - 操纵缓冲值而不是返回结果。副作用是并发编程中的主要问题之一,因为我们看到了 CachedThreadPool2.java 。InterferingTask 中的 val 被称为可变共享状态,这就是问题所在:多个任务同时修改同一个变量会产生竞争。结果取决于首先在终点线上执行哪个任务,并修改变量(以及其他可能性的各种变化)。

避免竞争条件的最好方法是避免可变的共享状态。我们可以称之为自私的孩子原则:什么都不分享。

使用 InterferingTask ,最好删除副作用并返回任务结果。为此,我们创建 Callable 而不是 Runnable :

// concurrent/CountingTask.java
import java.util.concurrent.*;
public class CountingTask implements Callable<Integer> {
    final int id;
    public CountingTask(int id) { this.id = id; }
    @Override
    public Integer call() {
    Integer val = 0;
    for(int i = 0; i < 100; i++)
        val++;
    System.out.println(id + " " +
        Thread.currentThread().getName() + " " + val);
    return val;
    }
}

call() 完全独立于所有其他 CountingTasks 生成其结果,这意味着没有可变的共享状态

ExecutorService 允许你使用 invokeAll() 启动集合中的每个 Callable:

// concurrent/CachedThreadPool3.java
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
public class CachedThreadPool3 {
    public static Integer extractResult(Future<Integer> f) {
        try {
            return f.get();
        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args)throws InterruptedException {
    ExecutorService exec =
    Executors.newCachedThreadPool();
    List<CountingTask> tasks =
        IntStream.range(0, 10)
            .mapToObj(CountingTask::new)
            .collect(Collectors.toList());
        List<Future<Integer>> futures =
            exec.invokeAll(tasks);
        Integer sum = futures.stream()
            .map(CachedThreadPool3::extractResult)
            .reduce(0, Integer::sum);
        System.out.println("sum = " + sum);
        exec.shutdown();
    }
}

输出结果:

0 pool-1-thread-1 100
2 pool-1-thread-3 100
3 pool-1-thread-4 100
1 pool-1-thread-2 100
4 pool-1-thread-5 100
6 pool-1-thread-7 100
5 pool-1-thread-6 100
7 pool-1-thread-8 100
9 pool-1-thread-10 100
8 pool-1-thread-9 100
sum = 1000

只有在所有任务完成后,invokeAll() 才会返回一个 Future 列表,每个任务一个 Future 。Future 是 Java 5 中引入的机制,允许你提交任务而无需等待它完成。在这里,我们使用 ExecutorService.submit() :

// concurrent/Futures.java
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.*;
public class Futures {
    public static void main(String[] args)throws InterruptedException, ExecutionException {
        ExecutorService exec
                =Executors.newSingleThreadExecutor();
        Future<Integer> f =
                exec.submit(new CountingTask(99));
        System.out.println(f.get()); // [1]
        exec.shutdown();
    }
}

输出结果:

99 pool-1-thread-1 100
100

注意:当你的任务在尚未完成的 Future 上调用 get() 时,调用会阻塞(等待)直到结果可用。

但这意味着,在 CachedThreadPool3.java 中,Future 似乎是多余的,因为 invokeAll() 甚至在所有任务完成之前都不会返回。但是,这里的 Future 并不用于延迟结果,而是用于捕获任何可能发生的异常。

还要注意在 CachedThreadPool3.java.get() 中抛出异常,因此 extractResult() 在 Stream 中执行此提取。

因为当你调用 get() 时,Future 会阻塞,所以它只能解决等待任务完成才暴露问题。最终,Futures 被认为是一种无效的解决方案,现在不鼓励,我们推荐 Java 8 的 CompletableFuture ,这将在本章后面探讨。当然,你仍会在遗留库中遇到 Futures。

我们可以使用并行 Stream 以更简单,更优雅的方式解决这个问题:

4 ForkJoinPool.commonPool-worker-5 100
1 ForkJoinPool.commonPool-worker-3 100
2 ForkJoinPool.commonPool-worker-1 100
8 ForkJoinPool.commonPool-worker-2 100
6 main 100
0 ForkJoinPool.commonPool-worker-6 100
7 ForkJoinPool.commonPool-worker-4 100
9 ForkJoinPool.commonPool-worker-7 100
3 ForkJoinPool.commonPool-worker-3 100
5 ForkJoinPool.commonPool-worker-5 100
1000

输出结果:

1 ForkJoinPool.commonPool-worker-3 100
8 ForkJoinPool.commonPool-worker-2 100
0 ForkJoinPool.commonPool-worker-6 100
2 ForkJoinPool.commonPool-worker-1 100
4 ForkJoinPool.commonPool-worker-5 100
9 ForkJoinPool.commonPool-worker-7 100
6 main 100
7 ForkJoinPool.commonPool-worker-4 100
5 ForkJoinPool.commonPool-worker-2 100
3 ForkJoinPool.commonPool-worker-3 100
1000

这不仅更容易理解,而且我们需要做的就是将 parallel() 插入到其他顺序操作中,然后一切都在同时运行。

2.4 Lambda 和方法引用作为任务

在 java8 有了 lambdas 和方法引用,你不需要受限于只能使用 Runnable 和 Callable 。因为 java8 的 lambdas 和方法引用可以通过匹配方法签名来使用(即,它支持结构一致性),所以我们可以将非 Runnable 或 Callable 的参数传递给 ExecutorService :

// concurrent/LambdasAndMethodReferences.java
import java.util.concurrent.*;
class NotRunnable {
    public void go() {
        System.out.println("NotRunnable");
    }
}
class NotCallable {
    public Integer get() {
        System.out.println("NotCallable");
        return 1;
    }
}
public class LambdasAndMethodReferences {
    public static void main(String[] args)throws InterruptedException {
    ExecutorService exec =
        Executors.newCachedThreadPool();
    exec.submit(() -> System.out.println("Lambda1"));
    exec.submit(new NotRunnable()::go);
    exec.submit(() -> {
        System.out.println("Lambda2");
        return 1;
    });
    exec.submit(new NotCallable()::get);
    exec.shutdown();
    }
}

输出结果:

Lambda1
NotCallable
NotRunnable
Lambda2

这里,前两个 submit() 调用可以改为调用 execute() 。所有 submit() 调用都返回 Futures ,你可以在后两次调用的情况下提取结果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值