上次的专栏简单的说了一下导致原子性,可见性,有序性的很容易看到的问题,有可能违背我们意愿的出现,这里就来说一下如何解决其中的可见性有序性导致的问题,也就引来了今天的问题——Java内存模型。
还有一点说就是,java内存模型,不要和之前说过的jvm内存模型混,不过确实这二者是有联系的,java内存模型更倾向于java并发方面的知识,而jvm内存模型是在java方面全部都会涉及的。
之前就说过volatile关键字,它可以让变量的修改直接写入主存来做到可见性,那么,其本质我们也知道,是因为缓存才导致无法实现可见性,也是因为编译优化,无法实现有序性。
那么最耿直的方法,禁用缓存和编译优化,就搞定了,但是我们程序的性能就堪忧了。所以我们需要按需禁用缓存和编译优化,那么问题是,如何做到按需求禁用?
java内存模型是一个很复杂的规范,本质上可以理解为,java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。从具体上来说,这些方法包括volatile、synchronized和final,以及六项Happen-Before原则。
volatile不是java的关键字,在c里面也有,意义就是禁用CPU缓存。
例如,我们声明一个Volatile变量:
volatile int x = 0;
这就是在告诉我们的编译器,对这个变量的读写,不能使用cpu缓存,必须直接写入内存。虽然很简单,但是也很难。
例如下面这段代码:
假如我们创建一个线程A执行write()方法,再创建一个线程B执行reader()方法,我们来想想x到底是多少。
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这⾥x会是多少呢?
}
}
}
其实有很多种答案,低于java1.5版本,可能是42也可能是0,因为x=42可能会被cpu缓存,所以会出现0,但是java1.5之后修复了这个bug,所以只可能是42了。
修改的方法是增强了volatile的语意,而增强的方法是依据Happens-Before原则
按照字面意思来说,也就是先行发生的意思,但是Happens-Before真正的意思是:前面一个操作的结果对于后续是可见的。用比较正式的说法来说,Happens-Before虽然约束了编译器的优化,但是又允许编译器优化,只需要保证Happens-Before原则即可。
接下来,就来说说Happens-Before的六项原则:
这条规则是说,在线程中,按照程序执行的顺序,前面的代码,Happens-Before于后面的代码。比如x=42 Happens-Before于v=true 并且前面的代码修改之后对于后面的代码是可见的。
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这⾥x会是多少呢?
}
}
}
一个volatile的写操作 Happens-Before于后续对这个volatile变量的读操作。
如果单纯理解这句话,还是禁用缓存,强制写入主存的意思,但是可以再看看第三条。
其实这条可以用夹逼定理来理解,如果A Happens-Before B , B Happens-Before C 那么 A Happens-Before C。
从图中我们可以看出:
(1) x=42 Happens-Before 写变量 v = true ←这是规则1的内容。
(2) 写变量v = true Happens-Before 读变量 v=true ←这是规则2的内容。
然后根据这个传递性规则,我们可以得到结果 像x = 42 Happens-Before 读变量 v = true ,这意味着什么呢?
如果线程B读到了v = true ,那么线程A设置的x = 42 对线程B是可见的。也就是说,线程B能看到x == 42 。这也就是java1.5 之后对于volatile的增强一说。
这条规则是指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
管程:一种通用的同步原语,在java中指的是synchronized,synchronized是对管程的实现。
管程中的锁会在java中隐式实现,例如下面的代码,在进入同步块之前会自动加锁,而在同步块结束会自动释放锁,加锁以及释放锁,都是编译器帮我们实现的。
synchronized (this) { //此处⾃动加锁
// x是共享变量,初始值=10
if (this.x < 12) {
this.x = 12;
}
} //此处⾃动解锁
假如x的初始值为10,线程A执行完代码块后x的值会变成12,线程B进入代码块时,能够看到线程A对x的写操作,也就是线程B能够看到x==12.
意思是,当主线程A启动子线程B之后,子线程B能够看到主线程再启动子线程B之前的操作。
换句话就是说,如果线程A调用了线程B的start方法,那么start() Happens-Before于线程B中的任意操作。
Thread B = new Thread(()->{
// 主线程调⽤B.start()之前
// 所有对共享变量的修改,此处皆可⻅
// 此例中,var==77
});
// 此处对共享变量var修改
var = 77;
// 主线程启动⼦线程
B.start();
其实经常写多线程编程的就应该很熟悉了,意思就是等待某个线程的任务结束之后才能继续下一个线程的任务。
换句话说就是,如果在线程A中,调用线程B的 join() 并成功返回,那么线程B中的任意操作Happens-Before 于该 join() 操作的返回。
Thread B = new Thread(()->{
// 此处对共享变量var修改
var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程B可⻅
// 主线程启动⼦线程
B.start();
B.join()
// ⼦线程所有对共享变量的修改
// 在主线程调⽤B.join()之后皆可⻅
// 此例中,var==66
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否发生中断。
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
前面说 volatile 为的是禁用缓存以及编译优化,那么有没有方法能让编译器优化的更好一点呢?
就是使用final
final修饰变量的时候,初衷就是告诉编译器,这个编程生而不变。
当然了,在1.5以后Java内存模型对final类型变量的重排进行了约束。现在只要我们提供正确构造函数没 有“逸出”,就不会出问题了。
通过内存屏障(memory barrier)禁止重排序的,即时编译器根据 具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令。对于编译器而言,内存屏障将它所 能做的重排序优化。而对于处理器而言,内存屏障将会导致缓存的刷新操作。比如,对于volatile,编译 器将在volatile字段的读写操作前后各插入一些内存屏障。
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- igbc.cn 版权所有 湘ICP备2023023988号-5
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务