内存模型概念
高速缓存存在的意义:临时变量都存在于内存中,当CPU执行程序时,速度是非常快的,但是牵扯到和内存交互,存取数据(耗时),会影响程序的执行速度,因此,必须引入高速缓存。当执行程序时,会先从内存中读取值,然后复制给高速缓存,之后程序执行,并将结果写入高速缓存,最后,再将缓存中的值刷新进入内存。高速缓存的存在,提高了程序的执行速度
引入的问题
多线程程序中,各个线程都有自己的高速缓存,因此当他们读取内存中的同一个值时(即共享变量),无法保证读取进缓存和写入内存时序,即缓存不一致,就会产生错误
如何解决?
- 总线加lock
- 通过缓存一致性协议。其中一个协议的思想为:当一个线程修改了值并将其写入内存之后,会通知其他线程,将其他线程中的缓存置为无效状态,当其他线程需要读取该值时,必须重新从内存中读取
三个性质
-
原子性
指一个或者多个操作,要么全部执行并且执行过程中不被任何因素打断,要么就全部都不执行 -
可见性
指当多个线程访问同一个变量时,一旦其中的一个线程改变了该变量的值,其他的线程立刻就可以看到修改后的值
个人认为可以简单理解为,当一个线程修改了值后,会立即将该值从高速缓存中写入内存,确保其他线程可以正确的读取到该值 -
有序性
Java程序执行时,为了提高执行效率,会发生指令重排。
具体的说,就是处理器不会保证代码会严格按照语句的先后顺序执行,而是会保证其代码的执行结果和严格按照顺序执行语句的结果一致。
例如:
Person p = new Person()
大致会做三件事:
- 给Person的实例分配内存空间
- 调用Person()的构造函数,初始化成员字段
- 将person对象指向分配的内存空间(此时,person就不是null了)
但是,由于上述乱序的存在,上述过程的2和3顺序是无法保证的
当在多线程情况下,执行顺序是1-3-2时,当执行完1-3后,切换至线程B,此时得到的p并未完全初始化, 在使用时就会有问题,这也是DLC单例在多线程环境下必须加volatile修饰instance的原因
注意:要让程序在多线程情况下正确的执行,那就必须同时满足上述三个条件,若其中一个不满足,就可能会导致运行结果不正确
Java的内存模型
Java中规定,所有的变量都是存在主存中,每一个线程都有自己的工作内存(相当于高速缓存),在程序执行时,线程对变量的所有操作都必须在其工作内存中
- 原子性
Java只会保证所有的基本类型数据,其简单的读取,赋值操作是原子的,变量之间的赋值不是原子的
若要其他类型数据或者操作实现原子性,可以使用sychronized和lock关键字实现
volatile变量并不能提供原子性 - 可见性
Java提供volatile关键字保证可见性(只针对共享变量而言)
通过volatile修饰,Java保证对值的修改会立刻刷新至主存中,当其他线程需要读取时(不确定是否一定会从主存中重新读取?),就会去读取新值
同样的,也可使用sychronized和lock关键字实现 - 有序性
可以通过volatile关键字实现部分有序性
可以使用sychronized和lock关键字实现
volatile
如果一个域声明为volatile,那么编译器和虚拟机就知道该域可能是被另一个线程并发更新的
volatile修饰共享变量的意义:
- 保证对该域修改的可见性
- 禁止指令进行重排,具体是指:
- 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
- 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行
简单来说,就是volatile会保证volatile之前的语句一定会在volatile之后的语句之前执行,但是并不能保证前面的语句不会乱序执行,其后的语句不会乱序执行
即是指:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效
使用场景:
由于volatile无法保证原子性,因此,只有在明确可以确保原子性的情况下,可以使用volatile,其效率会高于sychroniezd关键字,例如DCL单例中使用volatile和sychronized确保正确性
假设对于共享变量,除了赋值操作之外不会再有其他操作,那个可以将其声明为volatile
网友评论