Java内存模型
1、Java内存模型定义
- 描述多线程环境中线程与内存的关系
- Java内存模型定义了程序中各个变量的访问规则,即虚拟机将变量存储到内存和从内存取出变量的底层细节。
- 这里的变量可以理解为堆和方法区的,不包括线程私有的栈。
- 解决了多线程之间共享变量的可见性以及如何在需要的时候对共享变量进行同步。
- 用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果
- 先定义x,y,x_read,y_read都等于0
- 然后启动线程1和线程2
- (x_read,y_read)可能是右边4种结果,(0,0)也可能
- 一个线程内的代码大概率是按照顺序执行的,即这里线程1要执行的代码顺序肯定是先x=1,再y_read=y,而大概率不会反过来。但真反过来了(线程要执行的代码乱序执行)也是会发生的,就出现了(0,0),只不过这个反过来的概率很小。
- 由于内存可见性也会出现(0,0),即线程1执行了x=1,把结果写入了自己的寄存器,但还没有写入内存,(什么时候写入内存呢?这个不确定)此时线程2读取内存看到x自然就是0。
如何解决内存可见性的问题?因此有了Java内存模型
2、Java内存模型是一个规范,思想
- 首先定义了一个关系:happens-after关系
即:如果操作执行顺序具有先后性,那么后执行的操作能够看到先执行的操作(在内存中)的结果。
JVM自动保证以下操作遵守happens-after关系,⑴⑵最重要
- Unlock发生在Lock之前,即第一个人拿了锁做了一些事情,释放了锁之后,第二个人再拿这个锁,必须知道第一个人做的结果
- 写volatile发生在读volatile之前,即加了volatile的变量的变化都是所有线程可见的
- 线程start()发生在线程所有动作之前
- 线程中所有操作发生在线程join之前
- 构造函数完成发生在finalizer开始之前
上面我们随便定义的x,y,x_read,y_read并不遵守这个happens-after关系
与程序员密切相关的happens-before规则如下:
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中任意的后续操作。
- 监视器锁规则:对一个锁的解锁操作,happens-before于随后对这个锁的加锁操作。
- volatile域规则:对一个volatile域的写操作,happens-before于任意线程后续对这个volatile域的读。
- 传递性规则:如果 A happens-before B,且 B happens-before C,那么A happens-before C。
3、线程之间的通信
- 线程之间的通信机制有两种共享内存和消息传递
- 典型的共享内存通信方式就是通过共享对象进行通信
- 在java中典型的消息传递方式就是wait()和notify()。
4、主内存与工作内存(本地内存)
- Java线程之间的通信采用的是共享内存模型,这里提到的共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入何时对另一个线程可见。
- JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。
5、内存间交互操作
- JMM定义了8种操作,保证这8种操作都是原子的。包括lock,unlock,read,load,use,assign,store,write
- 同时定义了8种操作必须满足的规则,总结下来就是先行发生原则。
6、共享对象的可见性
- 当多个线程同时操作同一个共享对象时,如果没有合理的使用volatile和synchronization关键字,一个线程对共享对象的更新有可能导致其它线程不可见。
- volatile 关键字可以保证变量会直接从主存读取,而对变量的更新也会直接写到主存。
7、竞争现象
- 如果多个线程共享一个对象,如果它们同时修改这个共享对象,这就产生了竞争现象。
- 如下图,CPUa和CPUb同时,真正意义上的同时,对obj.count进行了+1操作,即使加了volatile,最终主内存中的obj.count也只会等于2
- 解决这个问题只能用synchronized
- 为什么volatile无效?volatile不是内存修改可见吗?但面对真正的同时,volatile也没办法
- 因为volatile关键字解决的是内存可见性的问题;synchronized关键字解决的是执行控制的问题
8、volatile和synchronized的区别
- volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;
- synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住;
- volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞;
- volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性;
- volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。
9、典型的volatile使用场景
要使用volatile,必须同时满足两个条件:
- 对变量的写操作不依赖当前值!!!!!!!!!!!!!
- 该变量没有包含在具有其他变量的不变式中。
- 例子1;
- volatile boolean shutdownFlag;
while(!shudownFlag){
do something;
}
- 这是一个典型的volatile使用场景,如果不加volatile,当shutdownFlag被另一个线程修改时,执行判断的线程却发现不了,就无法及时退出循环。
- 例子2:
- 单例模式双重锁检查中使用volatile
- 在并发情况下,如果没有volatile关键字,在第5行会出现问题
- 对于第5行可以分解为3行伪代码:
- 1. memory=allocate();// 分配内存 相当于c的malloc
2. ctorInstanc(memory) //初始化对象
3. instance=memory //设置instance指向刚分配的地址 - 上面的代码在编译器运行时,可能会出现重排序 从1-2-3 排序为1-3-2
- 如此在多线程下就会出现问题
- 例如现在有2个线程A,B。线程A在执行第5行代码时,B线程进来,而此时A执行了 1和3,没有执行2,此时B线程判断instance不为null 直接返回一个未初始化的对象,就会出现问题。
- 而用了volatile,就会禁止重排序,就不会出现上述问题。
例子
- 注意private Connetcion conn =null;这一句没有加volatile。
- 因此会导致synchronized外面的if(conn==null)这句判断出错。即Connection的构造函数乱序,线程看到conn不等于null,但其实Connection的构造函数还没运行完,因此我们会获得一个构建到一半的Connection,这个Connection并不能用。
- 为了解决这个问题,要改写成private volatile Connetcion conn =null。