什么是内存模型?我们为什么需要一个内存模型?
我们之前在详解synchronized、volatie和final的文章当中讲过,底层JVM、编译器、处理器执行指令的顺序并不保证和程序员所写的代码的顺序一样。在单线程中,只要遵从as-if-serial语义,就可以进行重排,所以对于程序员来说,底层的实现只是造成了一个程序是按照自己的代码顺序执行的假象。
在多线程环境中,如果不进行额外的synchronization的操作,像单线程一样的线性执行是没有办法得到保证的。JMM的作用有两方面:
- 要给开发者提供一个尽可能简单的,可以预测的多线程开发方式。
- 同时又要尽可能减少对底层JVM、硬件的限制,在保证1的前提下,对程序进行优化。
在一个共享内存的多处理器架构下,每个处理器都会有自己缓存,这些处理器会定期地从主内存里读取数据来更新自己的缓存。而由于硬件上的差异,不同的处理器所提供的缓存一致性是不一样的。这就需要一个内存模型来告诉程序,其可以从内存系统中得到什么保证,并且可以通过插入一下特殊指令,比如内存屏障(memory barriers),来保证在访问共享变量的时候能够得到一些额外的保证。
为了让开发者不需要接触到这些底层的不同和实现细节,JMM提供了一系列保证来判断在多线程环境下指令发生的先后顺序。同时,JVM处理了JMM与不同平台内存系统之间的差异性,通过在适当的地方插入内存屏障,来保证JMM对开发者的保证不会变。
简而言之,JMM定义了在多线程环境下所有操作的一个偏序。所谓偏序,即集合里的任意两个元素不一定能互相比较,这个集合当中,只有一部分元素的先后顺序是可以确定的。而这种保证,就是happens-before规则。
Happens-Before规则
Happens-Before规则提供了以下保证,摘抄Java Concurrency in Practice原文如下:
- Program order rule. Each action in a thread happens-before every action in that thread that comes later in the program order.
- Monitor lock rule. An unlock on a mointor lock happens-before every subsequent lock on that same monitor lock. This in true for both explicit lock and intrinsic lock.
- Volatile variable rule. A write to a
volatilefield happens-before every subsequent read of that same field. - Thread start rule. A call to
Thread.starton a thread happens-before every action in the started thread. - Thread termination rule. Any action in a thread happens-before any other thread detects that thread has terminated, either by successfully return from
Thread.joinor byThread.isAlivereturning false. - Interruption rule. A thread calling
interrupton another thread happens-before the interrupted thread detects the interrupt (either by haveInterruptedException thrown, or invokingisInterruptedorinterrupted). - Finalizer rule. The end of a constructor for an object happens-before the start of the finalizer for that object.
- Transitivity. If A happens-before B, and B happens-before C, then A happens-before C.
以上八条规则虽然简单,但在现实中用于判断不同线程之前的先后关系和可见性的时候十分方便。我们用在这篇文章中用过的例子:
1 | public class Demo { |
假设线程A先执行了main方法,并且对ready进行写入。之后线程B执行run()方法读取ready的值,此时number的值对线程B可见吗?我们用happens-before规则判断:
- 由规则3可知,第15行先与第7行。
- 由规则1可知,第7行先与第8行。
- 由规则1可知,第14行先与第15行。
- 由规则8可知,第14行先与第15行,先于第7行,先于第8行。所以对
number赋值42是对线程B可见的。
我们刚刚说过了,JMM定义的顺序只是集合的偏序,也就是任何两个不满足happens-before的操作,其先后顺序是没有任何保障的。
除此之外,JMM还对一些class library提供了happens-before保证:
- Placing an item in a thread-safe collection happens-before another thread retrieves that item from the collection.
- Counting down on a
CountDownLatchhappens-before a thread returns fromawaiton that latch. - Releasing a permit to a
Semaphorehappens-before acquiring a permit from that sameSamephore. - Actions taken by the task represented by a
Futurehappens-before another thread successfully returns fromFuture.get. - Submittig a
RunnableorCallableto anExecutorhappens-before the task begins execution. - A thread arriving at a
CyclicBarrierorExchangerhappens-before the other threads are released from that same barrier or exchange point. IfCyclicBarrieruses a barrier action, arriving at the barrier happens-before the barrier action, which in turn happens-before threads are released from the barrier.
Safe Publication and Initialization
我们在详解final这篇文章中聊过这个问题,有多种方法可以保证safely publish an object。这里我们不细聊这个话题,来看一些例子。
我们可以通过保证原子性来保证对象被安全初始化:
1 |
|
synchronized保证了resource在初始化完成之前都不会被其他线程访问,所以这是线程安全的初始化。
我们也可以用static initializer来保证安全初始化:
1 |
|
Static initializer的安全性是JVM提供的保证,因为JVM保证static initializer在对象被其他线程访问之前就会完成。因为JVM在调用static initializer的时候会获取锁,这个所会在其他线程确认这个类被loaded的时候获取,所以可以保证static initializer的写都立马对其他线程可见。所以在不需要其他同步操作的情况下,就可以保证初始化安全性。
那么在结合上面两个技巧的情况下,我们可以实现不需要额外同步操作的线程安全版本的lazy initialization方法(另一种方法是DCL,我们在这篇文章中讲过):
1 |
|
ResourceHodler的resource域是static initialized的,所以其保证初始化的安全性。getResource只有在被第一次调用的时候,才会初始化resource,所以其为lazy initialization。