附录:并发底层原理 翻译至捕获异常

This commit is contained in:
AlfredAlan
2019-07-17 01:07:51 +08:00
parent 2de98a17cc
commit aa95791ee7

View File

@@ -5,14 +5,14 @@
> 尽管不建议您自己编写底层 Java 并发代码,但是这样通常有助于了解它是如何工作的。
[并发编程](./24-Concurrent-Programming.md) 章节中介绍了一些用于高级并发的概念,包括为 Java 并发编程而最新提出的,更安全的概念( parallel Streams 和 CompletableFutures )。本附录则介绍在 Java 中底层并发概念,因此在阅读本篇时,您能有所了解掌握这些代码。您还会将进一步了解并发的普遍问题。
[并发编程](./24-Concurrent-Programming.md) 章节中介绍了一些用于高级并发的概念,包括为 Java 并发编程而最新提出的更安全的概念( parallel Streams 和 CompletableFutures )。本附录则介绍在 Java 中底层并发概念,因此在阅读本篇时,您能有所了解掌握这些代码。您还会将进一步了解并发的普遍问题。
在 Java 的早期版本中, 底层并发概念是并发编程的重要组成部分。我们会着眼于围绕这些技巧的复杂性以及为何您应该避免它们而谈。 “并发编程” 章节展示最新的 Java 版本(尤其是 Java 8)所提供的改进技巧,这些技巧使得并发的使用,如果本来不容易使用,也会更容易些。
在 Java 的早期版本中, 底层并发概念是并发编程的重要组成部分。我们会着眼于围绕这些技巧的复杂性以及为何您应该避免它们而谈。 “并发编程” 章节展示最新的 Java 版本(尤其是 Java 8)所提供的改进技巧,这些技巧使得并发的使用,如果本来不容易使用,也会变得更容易些。
<!-- What is a Thread? -->
## 线程
## 什么是线程
并发将程序划分成分离的,独立运行的任务。每个任务都由一个 *执行线程* 来驱动,我们通常将其简称为 *线程* 。而一个 *线程* 就是操作系统进程中单一顺序的控制流。因此,单个进程可以有多个并发执行的任务,但是你的程序使得每个任务都好像有自己的处理器一样。这线程模型为编程带来了便利,它简化了在单一程序中处理变戏法般的多任务过程。操作系统则从处理器上分配时间到您程序的所有线程中。
@@ -26,14 +26,14 @@ Java 并发的核心机制是 **Thread** 类,在该语言最初版本中, **
* *thread-local variables* (线程本地变量)的存储区域
* 用于控制线程的状态管理变量
包括 `main()` 在内的所有代码都会在某个线程内运行。 每当调用一个方法时,当前程序计数器被推到该线程的栈上,然后栈指针向下移动以足够来创建一个栈帧,其栈帧里存储该方法的所有局部变量,参数和返回值。所有基本类型变量都直接在栈上,虽然方法中创建(或方法中使用)对象的任何引用都位于栈帧中,但对象本身存于堆中。这仅且只有一个堆,被程序中所有线程所共享。
包括 **main()** 在内的所有代码都会在某个线程内运行。 每当调用一个方法时,当前程序计数器被推到该线程的栈上,然后栈指针向下移动以足够来创建一个栈帧,其栈帧里存储该方法的所有局部变量,参数和返回值。所有基本类型变量都直接在栈上,虽然方法中创建(或方法中使用)对象的任何引用都位于栈帧中,但对象本身存于堆中。这仅且只有一个堆,被程序中所有线程所共享。
除此以外线程必须绑定到操作系统这样它就可以在某个时候连接到处理器。这是作为线程构建过程的一部分为您管理的。Java 使用底层操作系统中的机制来管理线程的执行。
### 最佳线程数
如果你查看第 24 章 [并发编程](./24-Concurrent-Programming.md) 中使用 *CachedThreadPool* 的用例,你会发现 **ExecutorService** 为每个我们提交的任务分配一个线程。然而,并行流(**parallel Stream**)在 [**CountingStream.java** ](https://github.com/BruceEckel/OnJava8-Examples/blob/master/concurrent/CountingStream.java
) 中只分配了 8 个线程id 中 1-7 为工作线程8 为 **`main()`** 方法的主线程,它巧妙地将其用作额外的并行流)。如果你尝试提高 **range()** 方法中的上限值,你会看到没有创建额外的线程。这是为什么?
) 中只分配了 8 个线程id 中 1-7 为工作线程8 为 **main()** 方法的主线程,它巧妙地将其用作额外的并行流)。如果你尝试提高 **range()** 方法中的上限值,你会看到没有创建额外的线程。这是为什么?
我们可以查出当前机器上处理器的数量:
@@ -51,7 +51,7 @@ public class NumberOfProcessors {
*/
```
在我的机器上使用英特尔酷睿i7我有四个内核每个内核呈现两个*超线程*(指一种硬件技巧,能在单个处理器上产生非常快速的上下文切换,在某些情况下可以使内核看起来像运行两个硬件线程)。虽然这是 “最近” 计算机上的常见配置(在撰写本文时),但你可能会看到不同的结果,包括 `CountingStream.java` 中同等数量的默认线程。
在我的机器上使用英特尔酷睿i7我有四个内核每个内核呈现两个*超线程*(指一种硬件技巧,能在单个处理器上产生非常快速的上下文切换,在某些情况下可以使内核看起来像运行两个硬件线程)。虽然这是 “最近” 计算机上的常见配置(在撰写本文时),但你可能会看到不同的结果,包括 **CountingStream.java ** 中同等数量的默认线程。
你的操作系统可能有办法来查出关于处理器的更多信息例如在Windows 10上按下 “开始” 键,输入 “任务管理器” 和 Enter 键。点击 “详细信息” 。选择 “性能” 标签,您将会看到各种各样的关于您的硬件信息,包括“内核” 和 “逻辑处理器” 。
@@ -94,7 +94,7 @@ public class ThreadSize {
}
```
只要你不断递交任务,`CachedThreadPool` 就会继续创建线程。将 `Dummy` 对象递交到 `execute()` 方法以开始任务,如果线程池无可用线程,则分配一个新线程。执行的暂停方法 `pause()` 运行时间必须足够长,使任务不会开始即完成(从而为新任务释放现有线程)。只要任务不断进入而没有完成,`CachedThreadPool` 最终就会耗尽内存。
只要你不断递交任务,**CachedThreadPool** 就会继续创建线程。将 **Dummy** 对象递交到 **execute()** 方法以开始任务,如果线程池无可用线程,则分配一个新线程。执行的暂停方法 **pause()** 运行时间必须足够长,使任务不会开始即完成(从而为新任务释放现有线程)。只要任务不断进入而没有完成,**CachedThreadPool** 最终就会耗尽内存。
我并不总是能够在我尝试的每台机器上造成内存不足的错误。在一台机器上,我看到这样的结果:
@@ -103,7 +103,7 @@ public class ThreadSize {
OutOfMemoryError: 2816
```
我们可以使用 `-Xss` 标记减少每个线程栈分配的内存大小。允许的最小线程栈大小是 64k:
我们可以使用 **-Xss** 标记减少每个线程栈分配的内存大小。允许的最小线程栈大小是 64k:
```shell
>java -Xss64K ThreadSize
@@ -124,7 +124,7 @@ Windows 操作系统默认栈大小是 320K我们可以通过验证它给出
OutOfMemoryError: 2816
```
你还可以使用 `-Xmx` 标志增加 JVM 的最大内存分配:
你还可以使用 **-Xmx** 标志增加 JVM 的最大内存分配:
```shell
>java -Xss64K -Xmx5M ThreadSize
@@ -137,7 +137,7 @@ OutOfMemoryError: 5703
### The WorkStealingPool
这是一个 `ExecutorService` ,它使用所有可用的(由JVM报告) 处理器自动创建线程池。
这是一个 **ExecutorService** ,它使用所有可用的(由JVM报告) 处理器自动创建线程池。
```java
// lowlevel/WorkStealingPool.java
@@ -185,9 +185,207 @@ ForkJoinPool-1-worker-2
<!-- Catching Exceptions -->
## 异常捕获
这可能会让你感到惊讶:
```java
// lowlevel/SwallowedException.java
import java.util.concurrent.*;
public class SwallowedException {
public static void main(String[] args)
throws InterruptedException {
ExecutorService exec =
Executors.newSingleThreadExecutor();
exec.submit(() -> {
throw new RuntimeException();
});
exec.shutdown();
}
}
```
这个程序什么也不输出(然而,如果你用 **execute** 方法替换 **submit()** 方法,你就将会看到异常抛出。这说明在线程中抛出异常是很棘手的,需要特别注意的事情。
你无法捕获到从线程逃逸的异常。一旦异常越过了任务的 **run()** 方法,它就会传递至控制台,除非您采取特殊步骤来捕获此类错误异常。
下面是一个抛出异常的代码,该异常会传递到它的 **run()** 方法之外,而 **main()** 方法会显示运行它时会发生什么:
```java
// lowlevel/ExceptionThread.java
// {ThrowsException}
import java.util.concurrent.*;
public class ExceptionThread implements Runnable {
@Override
public void run() {
throw new RuntimeException();
}
public static void main(String[] args) {
ExecutorService es =
Executors.newCachedThreadPool();
es.execute(new ExceptionThread());
es.shutdown();
}
}
/* Output:
___[ Error Output ]___
Exception in thread "pool-1-thread-1"
java.lang.RuntimeException
at ExceptionThread.run(ExceptionThread.java:8)
at java.util.concurrent.ThreadPoolExecutor.runW
orker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Work
er.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
*/
```
输出是(经过调整一些限定符以适应阅读)
```
Exception in thread "pool-1-thread-1" RuntimeException
at ExceptionThread.run(ExceptionThread.java:9)
at ThreadPoolExecutor.runWorker(...)
at ThreadPoolExecutor$Worker.run(...)
at java.lang.Thread.run(Thread.java:745)
```
即使在 **main()** 方法体内包裹 **try-catch** 代码块来捕获异常也不成功:
```java
// lowlevel/NaiveExceptionHandling.java
// {ThrowsException}
import java.util.concurrent.*;
public class NaiveExceptionHandling {
public static void main(String[] args) {
ExecutorService es =
Executors.newCachedThreadPool();
try {
es.execute(new ExceptionThread());
} catch(RuntimeException ue) {
// This statement will NOT execute!
System.out.println("Exception was handled!");
} finally {
es.shutdown();
}
}
}
/* Output:
___[ Error Output ]___
Exception in thread "pool-1-thread-1"
java.lang.RuntimeException
at ExceptionThread.run(ExceptionThread.java:8)
at java.util.concurrent.ThreadPoolExecutor.runW
orker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Work
er.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
*/
```
这会产生与前一个示例相同的结果:未捕获异常。
为解决这个问题,需要改变 **Executor** (执行器)生成线程的方式。 **Thread.UncaughtExceptionHandler** 是一个添加给每个 **Thread** 对象,用于进行异常处理的接口。
当该线程即将死于未捕获的异常时,将自动调用 **Thread.UncaughtExceptionHandler.uncaughtException()**
方法。为了调用该方法,我们创建一个新的 `ThreadFactory` 类型来让 **Thread.UncaughtExceptionHandler** 对象附加到每个它所新创建的 **Thread**(线程)对象上。我们赋值该工厂对象给 **Executors** 对象的 方法,让它的方法来生成新的 **ExecutorService** 对象:
```java
// lowlevel/CaptureUncaughtException.java
import java.util.concurrent.*;
class ExceptionThread2 implements Runnable {
@Override
public void run() {
Thread t = Thread.currentThread();
System.out.println("run() by " + t.getName());
System.out.println(
"eh = " + t.getUncaughtExceptionHandler());
throw new RuntimeException();
}
}
class MyUncaughtExceptionHandler implements
Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("caught " + e);
}
}
class HandlerThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
System.out.println(this + " creating new Thread");
Thread t = new Thread(r);
System.out.println("created " + t);
t.setUncaughtExceptionHandler(
new MyUncaughtExceptionHandler());
System.out.println(
"eh = " + t.getUncaughtExceptionHandler());
return t;
}
}
public class CaptureUncaughtException {
public static void main(String[] args) {
ExecutorService exec =
Executors.newCachedThreadPool(
new HandlerThreadFactory());
exec.execute(new ExceptionThread2());
exec.shutdown();
}
}
/* Output:
HandlerThreadFactory@4e25154f creating new Thread
created Thread[Thread-0,5,main]
eh = MyUncaughtExceptionHandler@70dea4e
run() by Thread-0
eh = MyUncaughtExceptionHandler@70dea4e
caught java.lang.RuntimeException
*/
```
额外会跟踪验证工厂对象创建的线程是否获得新 **UncaughtExceptionHandler** 。现在未捕获的异常由 **uncaughtException** 方法捕获。
上面的示例根据具体情况来设置处理对象。如果你明白你想要在任何地方使用相同的异常处理对象,一个更简单的方法是设置默认的未捕获异常处理对象,它定义在 **Thread** 类中作为一个 **static**(静态) 字段:
```java
// lowlevel/SettingDefaultHandler.java
import java.util.concurrent.*;
public class SettingDefaultHandler {
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(
new MyUncaughtExceptionHandler());
ExecutorService es =
Executors.newCachedThreadPool();
es.execute(new ExceptionThread());
es.shutdown();
}
}
/* Output:
caught java.lang.RuntimeException
*/
```
只有在每个线程没有设置异常处理对象时候,默认处理对象才会被调用。系统会检查每个线程的版本,如果没有找到,则检查是否线程组中有专门的 **uncaughtException()** 方法;如果都没有,就会调用 **defaultUncaughtExceptionHandler** 方法。
可以将此方法与 **CompletableFuture**s 的改进方法进行比较。
<!-- Sharing Resources -->
## 资源共享
你可以将单线程程序看作一个孤独的实体,在你的问题空间中移动并一次只做一件事。因为只有一个实体,你永远不会想到两个实体试图同时使用相同资源的问题:问题犹如两个人试图同时停放在同一个空间,同时走过一扇门,甚至同时说话。
通过并发,事情不再孤单,但现在两个或更多任务可能会相互干扰。如果您不阻止这种冲突,您将有两个任务同时尝试访问同一个银行帐户,打印到同一个打印机,调整相同的阀门,等等。
### 资源竞争
### 解决资源竞争
### 同步多个生产者
<!-- The volatile Keyword -->
## volatile关键字