Java并行编程!
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
Java并⾏编程!
多核处理器现在已⼴泛应⽤于服务器、台式机和便携机硬件。它们还扩展到到更⼩的设备,如智能电话和平板电脑。由于进程的线程可以在多个内核上并⾏执⾏,因此多核处理器为并发编程打开了⼀扇扇新的⼤门。为实现应⽤程序的最⼤性能,⼀项重要的技术就是将密集型任务拆分成可以并⾏执⾏的若⼲⼩块,以便最⼤程度利⽤计算能⼒。
传统上,处理并发(并⾏)编程⼀直很困难,因为您不得不处理线程同步和共享数据的问题。Groovy (GPar)、Scala 和 Clojure 社区的努⼒已经证明,⼈们对 Java 平台上并发编程的语⾔级⽀持的兴趣⼗分强烈。这些社区都尝试提供全⾯的编程模型和⾼效的实现,以屏蔽与多线程和分布式应⽤程序相关的难点。但不应认为 Java 语⾔本⾝在这⽅⾯逊⾊。Java Platform, Standard Edition (Java SE) 5 及后来的 Java SE 6 引⼊了⼀组程序包,可以提供强⼤的并发构建块。Java SE 7 通过添加并⾏⽀持进⼀步增强了这些构建块。
下⽂⾸先简单回顾了 Java 并发编程,从早期版本以来已经存在的低级机制开始。然后在介绍 Java SE 7 中由分解/合并框架提供的新增基本功能分解/合并任务之前,先介绍 java.util.concurrent 程序包添加的丰富基元。⽂中给出了这些新 API 的⽰例⽤法。最后,在结束之前对⽅法进⾏了讨论。
下⾯,我们假定读者拥有 Java SE 5 或 Java SE 6 编程背景。在此过程中,我们还将介绍 Java SE 7 ⼀些实⽤的语⾔发展。
Java 并发编程
传统线程
过去,Java 并发编程包括通过 ng.Thread 类和 ng.Runnable 接⼝编写线程,然后确保其代码以正确、⼀致的⽅式对共享可变对象进⾏操作并避免错误的读/写操作,同时不会产⽣由于锁争⽤条件所导致的死锁。以下是基本线程操作的⽰例:
Thread thread = new Thread() {
@Override public void run() {
System.out.println(">>> I am running in a separate thread!");
}
};
thread.start();
thread.join();
本⽰例中的代码所做的只是创建⼀个线程,该线程将⼀个字符串打印到标准输出流。主线程通过调⽤ join() 等待所创建的(⼦)线程完成。
这样直接操作线程对于简单⽰例来说是不错,但对于并发编程,这种代码很快就容易产⽣错误,尤其是当多个线程需要合作执⾏⼀个⼤型任务时。在这样的情况下,需要协调其控制流。
例如,某个线程执⾏的完成可能依赖于其他线程执⾏完成。通常⼈们熟知的⽰例是⽣产者/使⽤者的例⼦,如果使⽤者的队列已满则⽣产者应等待使⽤者,当队列为空时使⽤者应等待⽣产者。这⼀要求可通过共享状态和条件队列得到满⾜,但您仍需要通过对共享状态对象使⽤ng.Object.notify() 和 ng.Object.wait() 来使⽤同步,这很容易出错。
最后,⼀个常见的问题是对⼤段代码甚⾄是整个⽅法使⽤同步和提供互斥。尽管此⽅法可产⽣线程安全的代码,但由于排除实际上过长所引起的有限并⾏度,该⽅法通常导致性能变差。
正如计算中经常发⽣的那样,操作低级基元以实现复杂操作会打开错误之门,因此开发⼈员应想办法将复杂性封装在⾼效的⾼级库中。Java SE 5 正好为我们提供了这种能⼒。
java.util.concurrent 程序包的丰富基元
Java SE 5 引⼊了⼀个名为 java.util.concurrent 的程序包系列,Java SE 6 对其进⾏了进⼀步的增强。该程序包系列提供了以下并发编程基元、集合和特性:
执⾏器是对传统线程的增强,因为它们是从线程池管理抽象⽽来的。它们执⾏与传递到线程的任务类似的任务(实际上,可封装实现ng.Runnable 的实例)。有些实现提供了线程池和调度策略。⽽且,可以通过同步和异步⽅式获取执⾏结果。
线程安全队列允许在并发任务之间传递数据。底层数据结构和并发⾏为有着丰富的实现,底层数据结构的实现包括数组列表、链接列表或双端队列等,并发⾏为的实现包括阻塞、⽀持优先级或延迟等。
细粒度的超时延迟规范,因为 java.util.concurrent 程序包中的⼤部分类均⽀持超时延迟。例如,如果任务在规定时间范围内⽆法完成,执⾏器将中断任务执⾏。
丰富的同步模式,不仅仅是 Java 中低级同步块所提供的互斥。这些模式包括信号或同步障碍等常⽤语法。
⾼效、并发的数据集合(映射、列表和集),通过使⽤写时复制和细粒度锁通常可在多线程上下⽂中产⽣出⾊的性能。
原⼦变量,可以使开发⼈员免于亲⾃执⾏同步访问。这些变量封装了常⽤的基元类型,如整型或布尔值,以及对其他对象的引⽤。
超出固有锁所提供的锁定/通知功能范围的多种锁,例如,⽀持重新进⼊、读/写锁定、超时或基于轮询的锁定尝试。
例如,考虑以下程序:
注意:由于 Java SE 7 引⼊的新的整数⽂本,可以在任意位置插⼊下划线以提⾼可读性(例如,1_000_000)。
import java.util.*;
import java.util.concurrent.*;
import static java.util.Arrays.asList;
public class Sums {
static class Sum implements Callable<Long> {
private final long from;
private final long to;
Sum(long from, long to) {
this.from = from;
this.to = to;
}
@Override
public Long call() {
long acc = 0;
for (long i = from; i <= to; i++) {
acc = acc + i;
}
return acc;
}
}
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(2);
List <Future<Long>> results = executor.invokeAll(asList(
new Sum(0, 10), new Sum(100, 1_000), new Sum(10_000, 1_000_000)
));
executor.shutdown();
for (Future<Long> result : results) {
System.out.println(result.get());
}
}
}
该⽰例程序利⽤执⾏器来计算多个长整型的和。内部 Sum 类实现了执⾏器⽤于计算结果的 Callable 接⼝,并发⼯作在 call() ⽅法内执⾏。java.util.concurrent.Executors 类提供了多种实⽤⽅法,如提供预配置执⾏器或将传统 ng.Runnable 对象封装到 Callable 实例中。与Runnable 相⽐,使⽤ Callable 的优势在于 Callable 能够显式返回⼀个值。
本⽰例使⽤⼀个执⾏器将⼯作分派给两个线程。ExecutorService.invokeAll() ⽅法接受 Callable 实例的集合,并在返回之前等待所有这些实例完成。它会返回 Future 对象的列表,这些对象全都表⽰计算的“未来”结果。如果我们以异步⽅式⼯作,就可以测试每个 Future 对象来检查其对应的 Callable 是否已完成⼯作,并检查其是否引发了异常,甚⾄可以取消其⼯作。相反,当使⽤普通传统线程时,必须通过共享的可变布尔值对取消逻辑进⾏编码,并由于定期检查此布尔值⽽减缓代码的执⾏。因为 invokeAll() 容易产⽣阻塞,我们可以直接对 Future 实例进⾏遍历并读取其计算和。
还需注意,必须关闭执⾏器服务。如果未关闭,则在主⽅法退出时 Java 虚拟机将不会退出,因为环境中还有活动线程。
分解/合并任务
概述
与传统线程相⽐,执⾏器是⼀⼤进步,因为可以简化并发任务的管理。有些类型的算法要求任务创建⼦任务并与其他任务互相通信以完成任务。这些是“分⽽治之”的算法,也称为“映射归约”,类似函数语⾔中的齐名函数。其思路是将算法要处理的数据空间拆分成较⼩的独⽴块。这是“映射”阶段。⼀旦块集处理完毕之后,就可以将部分结果收集起来形成最终结果。这是“归约”阶段。
⼀个简单的⽰例是您希望计算⼀个⼤型整数数组的总和(参见图 1)。假定加法是可交换的,可以将数组划分为较⼩的部分,并发线程对这些部分计算部分和。然后将部分和相加,计算总和。因为对于此算法,线程可以在数组的不同区域上独⽴运⾏,所以与对数组中每个整数循环执⾏的单线程算法相⽐,此算法在多核架构上可以看到明显的性能提升。
图 1:整数数组的部分和
使⽤执⾏器解决以上问题很简单:将数组分为 n 个可⽤物理处理单元,创建 Callable 实例以计算每个部分和,将部分和提交给管理 n 个线程的线程池的执⾏器,然后收集结果以计算最终和。