Java核心技术1-线程并发

  |  

摘要: 本文是《Java核心技术 10th》中关于线程并发的要点总结。

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


我们知道操作系统中有多任务的概念(multitasking):在同一刻运行多个程序的能力。

现在我们的计算机很多都有多个 CPU,但是并发执行的进程数目并不是由 CPU 数目制约的。操作系统将 CPU 的时间片分配给每一个进程,给人并行处理的感觉。

多线程程序在较低的层次上扩展了多任务的概念:一个程序同时执行多个任务。

多进程与多线程有以下几个区别:

  1. 本质的区别在于每个进程拥有自己的一整套变量,而线程则共享数据。
  2. 共享变量使线程之间的通信比进程之间的通信更有效、更容易。
  3. 在有些操作系统中,与进程相比较,线程更“轻量级”,创建、撤销一个线程比启动新进程的开销要小得多。

在实际应用中,多线程的应用很广。例如:

  1. 一个浏览器可以同时下载几幅图片
  2. 一个 Web 服务器需要同时处理几个并发的请求。
  3. 图形用户界面(GUI)程序用一个独立的线程从宿主操作环境中收集用户界面的事件。

$1 线程引入的例子

完整需求如下:

当点击 Start 按钮时,程序将从屏幕的左上角弹出一个球,这个球便开始弹跳。Start 按钮的处理程序将调用 addBall 方法。这个方法循环运行 1000 次 move。每调用一次 move,球就会移动一点,当碰到墙壁时,球将调整方向,并重新绘制面板。

$1-1 Baseline 代码

  • bounce/BallComponent.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package bounce;

import java.awt.*;
import java.util.*;
import javax.swing.*;

public class BallComponent extends JPanel {
private static final int DEFAULT_WIDTH = 450;
private static final int DEFAULT_HEIGHT = 350;

private java.util.List<Ball> balls = new ArrayList<>();

public void add(Ball b) {
balls.add(b);
}

public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
for(Ball b: balls) {
g2.fill(b.getShape());
}
}

public Dimension getPreferredSize() {
return new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
}
  • bounce/Ball.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

package bounce;

import java.awt.geom.*;

public class Ball {
private static final int XSIZE = 15;
private static final int YSIZE = 15;
private double x = 0;
private double y = 0;
private double dx = 1;
private double dy = 1;

public void move(Rectangle2D bounds) {
x += dx;
y += dy;
if(x < bounds.getMinX()) {
x = bounds.getMinX();
dx = -dx;
}
if(x + XSIZE >= bounds.getMaxX()) {
x = bounds.getMaxX() - XSIZE;
dx = -dx;
}
if(y < bounds.getMinY()) {
y = bounds.getMinY();
dy = -dy;
}
if(y + YSIZE >= bounds.getMaxY()) {
y = bounds.getMaxY() - YSIZE;
dy = -dy;
}
}

public Ellipse2D getShape() {
return new Ellipse2D.Double(x, y, XSIZE, YSIZE);
}
}
  • bounce/Bounce.java

sleep 是 Thread 类的静态方法,用于暂停当前线程的活动。需要捕获 sleep 方法可能抛出的异常 InterruptedException。在一般情况下,线程在中断时被终止。因此,当发生 InterruptedException 异常时,run 方法将结束执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package bounce;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class Bounce {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new BounceFrame();
frame.setTitle("BounceThread");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}

class BounceFrame extends JFrame {
private BallComponent comp;
public static final int STEPS = 1000;
public static final int DELAY = 3;

public BounceFrame() {
comp = new BallComponent();
add(comp, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
addButton(buttonPanel, "Start", event -> addBall());
addButton(buttonPanel, "Close", event -> System.exit(0));
add(buttonPanel, BorderLayout.SOUTH);
pack();
}

public void addButton(Container c, String title, ActionListener listener) {
JButton button = new JButton(title);
c.add(button);
button.addActionListener(listener);
}

// 后续要放入单独线程执行的任务
public void addBall() {
try {
Ball ball = new Ball();
comp.add(ball);

for(int i = 1; i <= STEPS; i++) {
ball.move(comp.getBounds());
comp.paint(comp.getGraphics());
Thread.sleep(DELAY);
}
} catch (InterruptedException e) {
}
}
}

这个代码中的问题是在球完成 1000 次弹跳之前,用户界面事件例如点击 Close 按钮是无法响应的。

可以将移动球的代码放在单独的线程里。可以发起多个球,每个球都在自己的线程中运行。此外 AWT 的事件分派线程也将一直并行运行,处理用户界面的事件。

$1-2 将移动球放在单独的线程里

前面的基础版代码中,addBall 是移动球的部分,修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void addBall() {
Ball ball = new Ball();
comp.add(ball);
Runnable r = () -> {
try {
for(int i = 1; i <= STEPS; i++) {
ball.move(comp.getBounds());
comp.paint(comp.getGraphics());
Thread.sleep(DELAY);
}
} catch (InterruptedException e) {
}
};

Thread t = new Thread(r);
t.start();
}

$2 在单独线程中执行一个任务的过程

$2-1 将任务代码移到实现了 Runnable 接口的类的 run 方法中

1
2
3
public interface Runnable {
void run();
}

Runnable 是一个函数式接口,可以用 lambda 表达式建立实例。

1
Runnable r = () -> { task code };

$2-2 创建一个 Thread 对象

  • 由 Runnable 创建 Thread 对象
1
Thread t = new Thread(r);
  • 通过构建一个 Thread 类的子类定义一个线程
1
2
3
4
5
class MyThread extends Thread {
public void run() {
task code;
}
}

$2-3 启动线程

1
t.start();

$3 线程的相关概念

$3-1 线程的中断

run 执行完最后一句返回,或者出现没有捕获的异常时,线程将终止。

没有可以强制线程终止的方法。interrupt 方法可以用来请求终止线程。

中断状态及其检测

线程的中断状态是每一个线程都具有的 boolean 标志。当对一个线程调用 interrupt 方法时,线程的中断状态将被置位。

测试线程的终端状态是否被置位,首先获得当前线程的 Thread 对象,然后调用 isInterrupted 方法。

1
2
3
while(!Thread.currentThread().isInterrupted() && ...) {
...
}

但如果线程被阻塞,则无法检测中断状态。

当在一个被阻塞的线程(调用 sleep 或 wait)上调用 interrupt 方法时,阻塞调用将会被 InterruptedException 异常中断。

注意:中断一个线程只是引起它的注意,被中断的线程可以决定如何响应中断。例如有些重要线程在处理完异常后,继续执行而不理会中断。

如果要将中断作为一个终止的请求,这种线程的 run 方法的形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Runnable r = () -> {
try {
...
while(!Thread.currentThread().isInterrupted() && ...) {
// 一次工作迭代
Thread.sleep(delay); // 可能有 sleep 也可能没有
}
} catch (InterruptedException e){
// 线程在 sleep 或 wait 期间被中断
} finally {
// 必要的清理工作
}
// 退出 run 方法,线程终止
}

如果在中断状态被置位时调用 sleep 方法,它不会休眠,而是会将清除这一状态并抛出 InterruptedException。

两个检测中断状态的方法:

  1. Interrupted 方法是一个静态方法,它检测当前的线程是否被中断。调用 interrupted 方法会清除该线程的中断状态。
  2. isInterrupted 方法是一个实例方法,可用来检验是否有线程被中断。调用这个方法不会改变中断状态。

$3-2 线程的状态

线程可以有如下 6 种状态:

  • New: 新创建
  • Runnable: 可运行
  • Blocked: 被阻塞
  • Waiting: 等待
  • Timed waiting: 计时等待
  • Terminated: 被终止

(1) 创建新线程

new Thread(r) 创建线程后,该线程还没开始运行。它的状态是 new。

(2) 可运行线程

调用 start() 方法,线程处于 Runnable 状态。

一个可运行的线程可能正在运行也可能没有运行,这取决于操作系统给线程提供运行的时间。

一旦一个线程开始运行,它不一定会保持运行。运行中的线程被中断,目的是为了让其他线程获得运行机会。线程调度的细节依赖于操作系统提供的服务。抢占式调度系统给每一个可运行线程一个时间片来执行任务。当时间片用完,操作系统剥夺该线程的运行权,并给另一个线程运行机会。当选择下一个线程时,操作系统考虑线程的优先级。

(3) 被阻塞线程和等待线程

当线程处于被阻塞或等待状态时,它暂时不活动。它不运行任何代码且消耗最少的资源。直到线程调度器重新激活它。共有三种:

  • 被阻塞状态:当一个线程试图获取一个内部的对象锁(而不是 java.util.concurrent 库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态。当所有其他线程释放该锁,并且线程调度器允许本线程持有它的时候,该线程将变成非阻塞状态。

  • 等待状态:当线程等待另一个线程通知调度器一个条件时,它自己进入等待状态。在调用 Object.wait 方法或 Thread.join 方法,或者是等待 java.util.concurrent 库中的 LockCondition 时,就会出现这种情况。

  • 计时等待状态:有几个方法有一个超时参数。调用它们导致线程进入计时等待(timed waiting)状态。这一状态将一直保持到超时期满或者接收到适当的通知。带有超时参数的方法有 Thread.sleepObject.waitThread.joinLock.tryLock 以及 Condition.await 的计时版。

下图是线程可以具有的状态以及从一个状态到另一个状态可能的转换:

(4) 线程的终止

线程因如下两个原因之一而被终止:

  1. 因为run方法正常退出而自然死亡。
  2. 因为一个没有捕获的异常终止了run方法而意外死亡。

$3-3 线程的属性

线程的优先级

认情况下,一个线程继承它的父线程的优先级。可以用 setPriority 方法提高或降低任何一个线程的优先级。

可以将优先级设置为在 MIN_PRIORITY(在 Thread 类中定义为 1)与 MAX_PRIORITY(定义为 10)之间的任何值。NORM_PRIORITY 被定义为 5。

线程优先级是高度依赖于系统的。当虚拟机依赖于宿主机平台的线程实现机制时,Java线程的优先级被映射到宿主机平台的优先级上,优先级个数也许更多,也许更少。例如,Windows 有 7 个优先级别。一些 Java 优先级将映射到相同的操作系统优先级。在 Oracle 为 Linux 提供的 Java 虚拟机中,线程的优先级被忽略: 所有线程具有相同的优先级。

除了依赖于系统外,使用线程优先级还有个问题就是低优先级线程可能被饿死的问题。因此线程优先级这个特性慎用。

守护线程

将线程 t 转换为守护线程的代码如下:

1
t.setDaeson(true);

守护线程的唯一用途是为其他线程提供服务。计时线程就是一个例子,它定时地发送“计时器嘀嗒”信号给其他线程或清空过时的高速缓存项的线程。当只剩下守护线程时,虚拟机就退出了。

守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。

未捕获异常处理器

线程的 run 方法不能抛出任何受查异常,但是,非受查异常会导致线程终止。在这种情况下,线程就死亡了。

在线程死亡之前,异常被传递到一个用于未捕获异常的处理器。该处理器必须属于一个实现 Thread.UncaughtExceptionHandler 接口的类。这个接口只有一个方法:

1
void UncaughtException(Thread t, Throwable e)

可以用 setUncaughtExceptionHandler 方法为任何线程安装一个处理器。也可以用 Thread 类的静态方法 setDefaultUncaughtExceptionHandler 为所有线程安装一个默认的处理器。

如果不安装默认的处理器,默认的处理器为空。

线程组

如果不为独立的线程安装处理器,此时的处理器就是该线程的 ThreadGroup 对象

线程组是一个可以统一管理的线程集合。默认情况下,创建的所有线程属于相同的线程组,也可能会建立其他的组。但注意:现在引入了更好的特性用于线程集合的操作,所以建议不要在自己的程序中使用线程组。

ThreadGroup 类实现 Thread.UncaughtExceptionHandler 接口。它的 uncaughtException 方法做如下操作:

  1. 如果该线程组有父线程组,那么父线程组的 uncaughtException 方法被调用。
  2. 否则,如果 Thread.getDefaultExceptionHandler 方法返回一个非空的处理器,则调用该处理器。
  3. 否则,如果 ThrowableThreadDeath 的一个实例,什么都不做。
  4. 否则,线程的名字以及 Throwable栈轨迹被输出到 System.err 上。

java.lang.Thread API 总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 休眠给定的毫秒数。
static void sleep(long millis)

// 构造一个新线程,用于调用给定目标的 run() 方法。
Thread(Runnable target)

// 启动这个线程,将引发调用run () 方法。这个方法将立即返回,并且新线程将并发运行。
void start()

// 调用关联Runnable的run方法。
void run()

// 向线程发送中断请求。线程的中断状态将被设置为 true。
// 如果目前该线程被一个 sleep 调用阻塞,那么,InterruptedException 异常被抛出。
void interrupt()

// 测试当前线程(即正在执行这一命令的线程)是否被中断。
// 注意,这是一个静态方法。这一调用会产生副作用 -- 它将当前线程的中断状态重置为 false。
static boolean interrupted()

// 测试线程是否被终止。不像静态的中断方法,这一调用不改变线程的中断状态。
boolean isInterrupted()

// 返回代表当前执行线程的 Thread 对象。
static Thread currentThread()

// 等待终止指定的线程。
void join()

// 等待指定的线程死亡或者经过指定的毫秒数。
void join(long millis)

// 得到这一线程的状态;NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING 或 TERMINATED 之一。
Thread.State getState()

// 停止该线程。这一方法已过时。
void stop()

// 暂停这一线程的执行。这一方法已过时。
void suspend()

// 恢复线程。这一方法仅仅在调用suspend()之后调用。这一方法已过时。
void resume()

// 设置线程的优先级。优先级必须在 Thread.MIN_PRIORITY 与 Thread.MAX_PRIORITY 之间。一般使用 Thread.NORM_PRIORITY 优先级。
void setPriority(int newPriority)

static int MIN_PRIORITY //线程的最小优先级。最小优先级的值为1。
static int NORM_PRIORITY //线程的默认优先级。默认优先级为5。
static int MAX_PRIORITY //线程的最高优先级。最高优先级的值为10。

// 导致当前执行线程处于让步状态。如果有其他的可运行线程具有至少与此线程同样高的优先级,那么这些线程接下来会被调度。注意,这是一个静态方法。
static void yield()

// 标识该线程为守护线程或用户线程。这一方法必须在线程启动之前调用。
void setDaemon(boolean isDaemon)

// 设置或获取未捕获异常的默认处理器。
static void setDefaultUncaughtExceptionHandler(Thread.UncaughtException Handler handler)
static Thread.UncaughtExceptionHandler getDefaultUncaughtException Handler()

// 设置或获取未捕获异常的处理器。如果没有安装处理器,则将线程组对象作为处理器。
void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler)
Thread.UncaughtExceptionHandler getUncaughtExceptionHandler()

Share