Java并发攻坚记(零) - 关键概念

Java并发这块是一个大主题,别说精通它,要彻彻底底把它的表层基础学完也是要花很长的时间,不过每种编程语言中关于并发的内容都是篇幅巨大的。
幸好,Java相较于C/C++,使用多线程进行编程要简单。但是,这不代表我们在Java中进行并发编程是一件简单轻松的工作,相反的,正如《Thinking In Java》这本书里并发那一章所说的那样:

实际的并发问题可能会以一种微妙而偶然的方式发生,因此并发看起来充满了危险;使用并发时,你得自食其力,并且只有变得多疑而自信,才能用Java编写出可靠的多线程代码。

并发编程中的陷阱是如此之多,我们稍有大意,就会立刻碰壁,我们是不是应该尽量回避有关这一个主题的代码呢?对于这个问题,书中也有回答到:

你无法选择何时在你的Java程序中出现线程。仅仅是你自己没有启动线程并不代表你就可以回避编写使用线程的代码。
例如,Web系统是最常见的Java应用系统之一,而基本的Web类库、Servlet具有天生的多线程性。

随着我们对编程越发的深入,接触多线程是无法避免的,并且它是引领我们通向更高层次的基石,所以无论如何,我们都必须要攻克它。

重点术语

操作系统中的概念

以下术语以及其相关的解释都是来自于《深入理解计算机系统》一书。

并发

如果逻辑控制流在时间上重叠,那么它们就是并发的(concurrent)。这种常见的现象称为并发(concurrency)
Logical control flows are concurrent if they overlap in time. This general phenomenon, known as concurrency.

进程

进程(process)是操作系统对一个正在运行的程序的一种抽象。
A process is the operating system’s abstraction for a running program.

进程的经典定义就是一个程序运行时的实例。
The classic definition of a process is an instance of a program in execution.

线程

线程(thread)就是运行在进程上下文中的逻辑流。线程由内核自动调度。
A thread is a logical flow that runs in the context of a process. The threads are scheduled automatically by the kernel.

每个线程都有它自己的线程上下文(thread context),包括一个唯一的整数线程ID(Thread ID, TID)、栈、栈指针、程序计数器、通用寄存器和条件码。
Each thread has its own thread context, including a unique integer thread ID (TID), stack, stack pointer, program counter, general-purpose registers, and condition codes.

逻辑控制流

由程序计数器的一系列值所组成的序列被称为逻辑控制流(logical control flow),或简称为逻辑流(logical flow)
This sequence of PC values is known as a logical control flow, or simply logical flow.

在计算机系统中逻辑流有许多不同的形式。异常处理程序、进程、信号处理程序、线程和Java进程都是逻辑流的例子。
Logical flows take many different forms in computer systems. Exception handlers,processes, signal handlers, threads, and Java processes are all examples of logical flows.

程序计数器

中央处理器(central processing unit, CPU)简称处理器,是解释(或执行)存储在主存中的指令的引擎。
The central processing unit (CPU), or simply processor, is the engine that interprets (or executes) instructions stored in main memory.
其核心是一个被称为程序计数器(program counter, PC)的字长大小的存储设备(或寄存器)。
At its core is a word-sized storage device (or register) called the program counter (PC).

在任何时刻,PC都会指向主存中的某条机器语言指令(即含有该指令的地址)。
At any point in time, the PC points at (contains the address of) some machine-language instruction in main memory.

程序计数器指示下一条被执行的指令在内存中的地址。
The program counter indicates the address in memory of the next instruction to be executed.

Java中的概念

以下术语以及其相关的解释都来自于《Java编程思想》与《实战Java高并发程序设计》两书。

同步与异步

同步(synchronous)和异步(asynchronous)通常用来形容一次方法调用。

Java的线程机制是抢占式的,这表示调度机制会周期性地终端线程,将上下文切换到另一个线程,从而为每个线程提供时间片,使得每个线程都会分配到数量合理的时间去驱动它的任务。

  • 一个Java程序总是从main方法开始,运行main方法的线程叫做主线程
  • Java中的线程大体分为两种:非后台线程(前台线程或用户线程)后台线程(daemon thread)

使用线程的动机

  • 创建响应更快的用户界面
  • 利用多处理器系统
  • 简化模型
  • 执行异步或后台处理

需要使用的API

在Java中使用线程,最主要使用的就是java.lang.Thread这个类以及java.lang.Runnable这个接口。

Thread类是一个具体类,这个类封装了线程的行为,并且它自身也实现了Runnable接口。

1
public class Thread implements Runnable { ... }

Runnable接口里面有一个run方法,实现该接口的类的实例都可以被当作一个任务给交给某个线程执行。

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

所以我们可以将Thread类称为一个线程或者也可以称为一个可以被执行的任务。

代码示例

在Java中有两种方式创建一个新的执行线程。
下面都是以新年倒数为例的代码。
程序十分简单,NewYearCountDown这个类,里面有一个任务计数器taskCount,还有一个id作为任务的标识。而在run方法里面进行新年倒数这个操作。

方式1:继承Thread类

我们可以通过继承Thread类生成它的子类,并重写它的run方法,最后实例化这个子类,并调用start方法启动这个新的线程。

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
public class NewYearCountDown extends Thread {
private static int taskCount = 0;
private final int id = taskCount++;

// 倒数秒数
private int countDown = 10;

public NewYearCountDown() { }

public NewYearCountDown(int countDown) {
this.countDown = countDown;
}

@Override
public void run() {
while (countDown-- > 0) {
System.out.println("#" + id + " -> " +
(countDown > 0 ? countDown : "Happy New Year " +
Calendar.getInstance().get(Calendar.YEAR)));
}
}

public static void main(String[] args) {
Thread t1 = new NewYearCountDown();
Thread t2 = new NewYearCountDown();
t1.start();
t2.start();
}
}

方式2:实现Runnable接口

Thread类有一个构造函数public Thread(Runnable target)可以接收一个Runnable类型的对象以此来进行实例化。
实例化完毕后,就和方式1一样,调用它的start方法启动线程。

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
public class NewYearCountDown implements Runnable {
private static int taskCount = 0;
private final int id = taskCount++;

// 倒数秒数
private int countDown = 10;

public NewYearCountDown() { }

public NewYearCountDown(int countDown) {
this.countDown = countDown;
}

@Override
public void run() {
while (countDown-- > 0) {
System.out.println("#" + id + " -> " +
(countDown > 0 ? countDown : "Happy New Year " +
Calendar.getInstance().get(Calendar.YEAR)));
}
}

public static void main(String[] args) {
Thread t1 = new Thread(new NewYearCountDown());
Thread t2 = new Thread(new NewYearCountDown());
t1.start();
t2.start();
}
}
输出结果:
#0 -> 9
#0 -> 8
#1 -> 9
#1 -> 8
#1 -> 7
#0 -> 7
#1 -> 6
#0 -> 6
#1 -> 5
#0 -> 5
#1 -> 4
#0 -> 4
#1 -> 3
#0 -> 3
#1 -> 2
#0 -> 2
#1 -> 1
#0 -> 1
#1 -> Happy New Year 2016
#0 -> Happy New Year 2016

程序细节

  • 两种方式相比之下,方式2更为常用且更为实用。Java不支持多重继承,所以,自定义类继承Thread类后就不能再继承其他类了。
    因此使用方式1不利于日后程序的再度扩展,所以一般情况下,我们都要选择方式2,并且方式2能够更为清晰地表达一种意图:
    创建一个任务,并将它赋予给线程进行驱动

  • 线程的执行数顺序与线程的启动顺序是无关的,start方法虽然是用于启动线程,但它只是将这个线程从新建状态转变为就绪状态,
    表示这个线程一切准备就绪,至于它会在什么时候运行要由线程调度器决定,所以上面这个程序每次得出的运行结果都不尽相同。

  • run方法运行完毕返回,运行它的线程就会结束。




现在,我们就可以自己就可以使用最基本的多线程技术进行并发编程了。
我们只需做到这几点:

  • 创建描述任务的类并实现Runnable接口,在run方法里面写上任务实际要执行的操作。
  • 使用Thread类创建线程实例,并将任务实例交给这个线程实例。
  • 调用线程实例的start方法。