Java并发编程艺术 - Java内存模型基础

并发编程的两个关键性问题

  • 线程通信
  • 线程同步

线程通信

通信是指线程间的信息交换,主要有两种:共享内存和消息传递。

共享内存是线程利用内存的公共状态进行隐私通信。

在Java中主要利用堆内存变量实现共享内存,包括静态域、数据、实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public 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
64
public class Consumer implements Runnable {

private PipedInputStream pis;

public Consumer(PipedInputStream pis) {
this.pis = pis;
}

@Override
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;
}

@Override
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关系,并不意味着一个操作必须在另一个之前,而是第一个操作的结果对第二个可见。

------ 本文结束------

本文标题:Java并发编程艺术 - Java内存模型基础

文章作者:Perkins

发布时间:2019年05月27日

原始链接:https://perkins4j2.github.io/posts/49164/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。