并发编程的两个关键性问题
- 线程通信
- 线程同步
线程通信
通信是指线程间的信息交换,主要有两种:共享内存和消息传递。
共享内存是线程利用内存的公共状态进行隐私通信。
在Java中主要利用堆内存变量实现共享内存,包括静态域、数据、实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public class ThreadShare {
//利用内存静态变量进行状态传递
static int num = 0;
private void increase() {
num++;
System.out.println(num);
}
public static void main(String[] args) {
ThreadShare threadShare = new ThreadShare();
new Thread(threadShare::increase).start();
new Thread(threadShare::increase).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
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
64public class Consumer implements Runnable {
private PipedInputStream pis;
public Consumer(PipedInputStream pis) {
this.pis = pis;
}
public void run() {
// 将数据保存在byte数组中
byte[] bytes = new byte[100];
try {
// 从数组中得到实际大小。
int length = pis.read(bytes);
System.out.println(new String(bytes, 0, length));
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
pis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class Producer implements Runnable {
private PipedOutputStream pos;
public Producer(PipedOutputStream pos) {
this.pos = pos;
}
public void run() {
try {
pos.write("Hello World".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
pos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public class TestPipedStream {
public static void main(String[] args) {
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream();
try {
// 连接管道
pos.connect(pis);
new Thread(new Producer(pos)).start();
new Thread(new Consumer(pis)).start();
} catch (IOException e) {
e.printStackTrace();
}
}
}
线程同步
在共享内存中必须显式指定某个方法或代码段进行线程互斥,在消息传递中隐式说明消息接收需在消息发送前。
互斥锁主要有Lock,synchronized。
JMM内存结构
线程AB间要通信,需要经过两步:
- A将共享变量X的本地内存刷新到主存
- B到主存更新本地内存的共享变量X
实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。
JMM通过控制主内存与每个线程的本地内存之间的交互,提供内存可见性保证。
指令重排
执行程序为提高性能,编译器和处理器会进行指令重排,主要有3种。
- 编译器在不影响单线程结果的前提下,进行指令重排。
- 指令并行重排。多条指令不存在数据依赖性,处理器可以改变机器指令执行顺序。
- 内存重排。处理器使用缓存和读写缓冲区进行加载和存储。
JMM通过插入内存屏障防止指令重排。
happens-before
JMM中,如果一个操作执行结果需要对另一个操作可见,两个操作需要存在happens-before关系。
该两个操作可以是一个线程也可以是不同线程。
happens-before规则如下:
- 程序顺序规则:一个线程每个操作,happens-before线程后续操作
- 监视器锁规则:一个监视器锁的解锁,happens-before锁的加锁
- volatile变量规则:一个volatile的写,happens-before任意对此域的读
- 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C
两个操作存在happens-before关系,并不意味着一个操作必须在另一个之前,而是第一个操作的结果对第二个可见。