天天看点

JMM-java内存模型

在并发编程中,需要处理的两个关键问题:

1、线程之间如何通信。

2、线程之间如何同步。

线程之间的通信机制有两种:1、共享内存。2、消息传递。

共享内存的并发模型里,线程之间共享程序的公共状态,通过读-写内存中的公共状态进行隐式通信。

消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消息来显式通信。

JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存。

堆内存在线程之间共享,所有实例域、静态域、数组元素都存储在堆内存中。

局部变量、方法定义参数、异常处理参数,不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

==>JMM通过控制主内存和每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。

StoreLoad Barriers "全能型"屏障,但执行开销昂贵,当前处理器通常要把写缓冲区的数据全部刷新到内存中。

使用happens-before的概念阐述操作之间的内存可见性。即一个执行的结果对另一个操作可见。这两个操作既可以是在一个线程之内,也可以在不同线程之间。

注意,两个操作之间具有happens-before关系,并不是意味着一个操作必须在另一个之前执行!这里仅仅要求一个操作对另一个可见,且前一个操作按顺序排在第二个之前。

在执行程序时,为了提高性能,编译器和处理器通常会对指令做重排序。有3种类型的重排序:1、编译器优化的重排序。2、指令级并行的重排序。3、内存系统的重排序。1属于编译器重排序,2、3属于处理器重排序。

编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。(单线程,多线程不保障)

数据依赖:两个操作同时访问同一个变量,其中有一个为写,那么这两个操作存在数据依赖。

as-if-serial不管怎么重排序,程序的执行结果不会改变,JMM允许这种重排序。

重排序对存在控制依赖的操作单线程没有影响,对存在控制依赖的多线程可能会改变程序的执行结果。

==>JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止指定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

==>编译器和处理器只能帮到这了,多线程的执行顺序要靠程序员自己进行同步控制。

==>对于未同步或未正确同步的多线程程序,JMM只提供最小的安全性:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,false,null),肯定不会是无中生有的值。为了实现最小的安全性,JVM在堆上分配对象时,首先会对内存空间清零,然后才会在上面分配对象。所以,在已清零的内存空间分配对象时,域的默认初始化就已经完成了。

但是,对64位数据是非原子写。

那么,JMM提供了哪些途径可以让程序员同步自己的多线程程序呢?

1 volatile

当声明共享变量为volatile之后,对这个变量的读/写,可以看成是使用同一个锁做了同步。

这意味着,即使是64位的long或double型变量,只要它是volatile变量,对该变量的读/些都是原子的。但,volatile++这种复合操作不具有原子性。

为啥volatile能同步操作呢?

volatile限制了编译器和处理器的重排序操作。–> volatile内存语义

而且,JSR-133还对volatile的内存语义做了增强:

严格限制编译器和处理器对volatile变量和普通变量的重排序,确保volatile的写-读和锁的释放-获取具有相同的内存语义。

2 锁

锁是java并发编程中最重要的同步机制。

锁:

1、让临界区互斥执行。

2、让释放锁的线程向获取同一个锁的线程发送消息。

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。

当线程获取锁时,JMM会把该线程对应的本地内存置为无效。

锁释放-获取的内存语义:同时具有volatile读和volatile写的内存语义。

至少有以下两种实现方式:

1)利用volatile变量的写-读所具有的内存语义。

2)利用CAS所附带的volatile读和写的内存语义。

==>以上也是整个current包实现的基石。

concurrent包的源代码,通用实现模式:

首先,声明共享变量为volatile。

然后,使用CAS的原子条件更新来实现线程之间的同步。

同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间通信。

3 final

与锁、volatile相比,对final域的读写更像是普通变量的访问。但编译器和处理器都要对final遵守两个重排序规则:

1、在构造函数内对一个final域的写入,与随后把这个构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

2、初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

==>JMM保证,如果构造函数里有final域,那么在构造函数返回前,被构造对象的引用不能为其他线程可见。

而且,JSR-133增强了final的语义,只要对象是正确构造的,那么不需要使用同步,就可以保证任意线程都能看到这个final域在构造函数中被初始化之后的值。

happens-before是JMM最核心的概念。

JMM其实在遵循一个原则:只要不改变程序的执行结果,编译器和处理器怎么优化都行。(指的是单线程程序和正确同步的多线程程序。)

例如:

(1)如果编译器经过细致的分析,认定一个锁只会被单个线程访问,那么这个锁可以被消除。

(2)如果编译器经过细致的分子,认定一个volatile变量只会被单个线程访问,那么这个编译器可以把这个volatile变量当做一个普通变量来对待。

继续阅读