共享内存模型

​ 在线程与锁之间,一个非常简单的并发模型是–共享内存模型。共享内存模型是指通过共享内存来实现并发的模型。
共享内存模型中,当多个线程在并发执行过程中对共享资源进行处理时候,如果没有进行一定的同步处理,那么就会出现读脏、数据无效的问题。在共享内存模型中,非常重要的两个概念是:内存可见性+原子性。
Java中多个线程之间的通信就是通过共享变量来进行的,JVM里面主内存是所有内存共享的,而每个线程拥有自己的工作内存,线程只能在自己的工作内存中处理数据。那么很明显的,在内存之间对共享变量处理过程就容易出现错误问题。
内存结构模型

内存可见性

​ 内存可见性保证了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
public class Counting {
public static void main(String[] args) throws InterruptedException{
class Counter {
private int count = 0;
public void increment() {
++count;
}
public int getCount() {
return count;
}
}
final Counter counter = new Counter();
class CountingThread extends Thread {
@Override
public void run() {
for (int x=0;x<10000;x++) {
counter.increment();
}
}
}
CountingThread t1 = new CountingThread();
CountingThread t2 = new CountingThread();
t1.start();t2.start();
t1.join();t2.join();
System.out.println(counter.getCount());
}buding
}
//程序来源:《Seven Concurrency Models in Seven Weeks》

​ 在上述代码中,有两个线程,分别对一个共享变量count进行+10000操作,如果程序是正常的,那么输出的值应该是20000,然而因为线程每次进行+1操作之后,修改的值对另一个线程不可见,而对数据进行更新到主内存的时间不定,所以多次测试结果都不相同。同样的,这个程序也违反了变量模型的原子性。

原子性

​ 并发的原子性表现为在多线程操作中,对于某一个共享变量的操作,每个线程的操作都应该是原子性的。例如在Java中,对于一个数据的自增,也就是X++,这个操作实际上是读-改-写模式的操作,当在多线程中操作的时候,可能会存在线程a进行读取。线程b进行读取,同时两个线程分别对其进行++操作,但是最终的结果可能是1,也就是++操作不是原子操作。
维持原子操作的方法,在共享内存模型中,是对共享内存进行加锁操作。通过加锁操作,持有一个共享对象的锁,将一个线程对该对象的操作转换为原子操作,保证程序的原子性。

非常诡异的现象

​ 在Java中,存在着一些非常有意思的操作:指令重排序(乱序执行)。当出现指令重排序的时候,
Java会打乱代码的执行顺序,也就是说,对于代码的执行会处于不可控状态,这个状态跟线程的乱序执行是一样的道理。

1
2
3
4
导致在执行过程中发生乱序执行的原因有这么几种情况:
编译器的静态优化可以打乱代码的执行顺序。
JVM的动态优化会打乱代码的执行顺序。
硬件可以通过乱序执行来优化其性能。

​ 乱序执行造成的影响:

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
public class Puzzle {
static boolean answerReady = false;
static int answer = 0;
static Thread t1 = new Thread(){
@Override
public void run() {
answer = 42;
answerReady = true;
}
};
static Thread t2 = new Thread(){
@Override
public void run() {
if (answerReady)
System.out.println("The meaning of life is: " + answer + ".");
else
System.out.println("I don't know the answer.");
}
};
public static void main(String[] args) throws InterruptedException{
t1.start();t2.start();
t1.join();t2.join();
}
}
//程序来源:《Seven Concurrency Models in Seven Weeks》

​ 上述代码中,正确的执行输出结果应该是:The meaning of life is: 42.但是当乱序执行的时候,可能会出现一种结果:The meaning of life is: 0.解决方案是Java中有些机制可以使程序在编译器、处理器优化时会对有数据依赖的禁止指令重排序,如:volatile、synchronized等。
共享内存模型主要要满足两点需求:内存可见性和原子性,满足这个条件的线程是安全的。