参考博客:
https://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html
https://www.cnblogs.com/dolphin0520/p/3920373.html
https://www.cnblogs.com/chengxiao/p/6528109.html
https://cyc2018.github.io/CS-Notes
《深入理解Java虚拟机》
Java内存模型:实现让 Java程序在各种平台下都能达到一致的内存访问效果。
主内存和工作内存
处理器上的寄存器的读写速度比内存快几个数量级,为了解决这种矛盾,在它们之间加入了高速缓存。
加入高速缓存就带来了一些问题,如缓存一致性。多个缓存共享同一块主内存区域,那么多个缓存的数据可能会不一致。
所有变量都存储在主内存中,每个线程还有自己的工作内存,工作内存存储在高速缓存或者寄存器中,保存了该线程使用的变量的主内存副本拷贝(所有的变量都存储在主内存中)。线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存中的变量。
线程只能直接操作工作内存中的变量,不同线程之间的变量值的传递需要通过主内存来完成。
内存间的交互操作
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节。 Java内存模型中定义了8个操作来完成主内存和工作内存之间的交互操作。
- read:把一个变量从主内存传输到工作内存中。
- load:在read之后执行,把read得到的值放入工作内存的变量副本中。
- use:把工作内存的一个变量传递给执行引擎。
- assign:把一个从执行引擎接收到的值赋给工作内存中的变量。
- store:把工作内存的变量的值传送到主内存中。
- write:在store之后执行,把store得到的值放入主内存的变量中。
- lock:作用于主内存的变量。
- unlock:作用于主内存的变量。
内存模型的三大特性
1. 原子性
Java内存模型保证了read、load、use、assign、store、write、lock和unlock操作具有原子性。但是Java内存模型允许将没有被volatile修饰的64位数据(long, double)的读写操作分为两步32位的操作来执行,即load、store、read和write操作可以不具备原子性。
关于volatile,我们来看一个问题。
有如下代码:
public class inc_exam {
private volatile int count = 0;
public void inc() {
count ++;
}
public int getInc() {
return count;
}
public static void main(String[] args) throws InterruptedException{
inc_exam exam = new inc_exam();
// 使用CountDownLatch来等待子线程执行完毕
CountDownLatch countDownLatch = new CountDownLatch(1000);
ExecutorService executorService = Executors.newCachedThreadPool();
for(int i = 0; i < 1000; i++) {
executorService.execute(()->{
exam.inc();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(exam.getInc());
}
}
关于countDownLatch可以看我的博客: Java并发之CountDownLatch、CyclicBarrier、Semaphore (AQS)
上列代码的作用是,使用1000个线程来执行exam对象的inc()操作,也就是使得volatile修饰的变量count的值增加。最后执行完毕之后,输出exam对象中的int变量count的值,我们发现,每次输出的值都不一样,而且都少于1000。
并且,当我们去除volatile关键字之后,发现,输出的结果也不是1000(肯定不是1000).
由此,我们有如下结论,volatile关键字只能保证变量的可见性(见下文), 但是并不能保证变量的原子性。
分析原因:我们将内存间的交互操作简化为3个,load、assign、store。所以,在上述代码中,对变量count执行自增操作,其实也是执行了多个操作。
当两个线程(这里以两个为例,T1,T2)同时对count进行操作,load、assign、store这一系列操作整体上并不具备原子性。因为在T1修改count并且还没有将修改后的值写入主内存时,T2依然可以读入旧值。两个线程虽然执行了两次自增运算,但是主内存中count的值最后还是1而不是2 (图示中的情况). 因此,对int类型读写操作满足原子性,只是说明load、assign、store这些单个操作具备原子性。
解决方案:
(1). 使用AtomicInteger,AtomicInteger能保证多个线程修改的原子性。
使用AtomicInteger实现上面的示例,只需修改如下部分:
private AtomicInteger count = new AtomicInteger();
public void inc() {
count.incrementAndGet();
}
public int getInc() {
return count.get();
}
最后输出结果为1000.
(2). 使用synchronized来保证操作的原子性。
示例代码修改部分为:
private int count = 0;
public void inc() {
synchronized (this) {
count ++;
}
}
public int getInc() {
return count;
}
(3). 使用重入锁ReentrantLock来保证操作的原子性。
示例代码修改部分为:
Lock lock = new ReentrantLock();
private int count = 0;
public void inc() {
lock.lock();
try {
count ++;
} finally {
lock.unlock();
}
}
public int getInc() {
return count;
}
2. 可见性
可见性指的是当一个线程修改了某个共享变量的值,其他线程能够立即得知这个修改。Java内存模型是通过在变量修改后,将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性。
实现可见性的三种主要方式:
- volatile, 被volatile修改的变量,就保证了该共享变量对所有线程的可见性。(不保证原子性)
- synchronized, 对一个变量执行unlock操作之前,必须把变量值同步回主内存。
- final,被final关键字修饰的字段在构造器中一旦初始化完成,并且没有发生this逃逸,那么其他线程就能看到final字段的值。
3. 有序性
有序性是指:在本线程中观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的。
Java内存模型中,允许编译器和处理器对指令进行重排序,重排序不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
Java中可以使用volatile和synchronized来保证有序性。其中volatile通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。 而synchronized,通过保证每个时刻只有一个线程执行同步代码,相当于是让线程顺序执行。
JVM之先行发生原则
JVM规定了先行发生原则,让一个操作无需控制就能优先于另一个操作完成。
- 单一线程原则。 在一个线程内,在程序前面的操作先行发生于后面的操作。
- Volatile变量规则。 对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
- 线程启动规则。 Thread 对象的 start() 方法的调用先行发生于此线程的每一个动作。
- 线程加入规则。 Thread 对象的结束先行发生于 join() 方法的返回。
- 线程中断规则。 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否中断发生。
- 对象终结规则。 一个对象的初始化完成 ( 析构函数执行结束 ) 先行发生于它的 finalize() 方法的开始。
- 传递性。 如果操作 A 先行发生于操作 B, 操作 B 先行发生于操作 C, 那么操作 A 先行发生于操作 C。
- 管程锁定规则。 一个unlock操作先行发生于后面对同一个锁的lock操作。