这里主要讲一下Java内存模型,以及涉及到的重排序概念,与程序员密切相关的happens-before,和volatile关键字起的作用
Java内存模型
这里简单介绍一下Java内存模型,只是抽象的帮助理解,实际和寄存器,内存交互并不是这样的。
正确理解java内存模型才能明白线程可见性的问题
java并发采用的是共享内存模型
,虽然是共享了,可是还是需要将线程的本地内存推到主内存并让其他线程去读才算是共享成功了。。这里本地内存的数据包含栈内和堆内数据等
线程通信更像是这样的,线程A在本地修改了x的值,然后推向主内存,然后线程B去主内存读取x的最新值
重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行的重新排序的一种手段
JMM对于重排序的原则是只要不改变程序运行的结果,那就可以尽量优化。
重排序对于编译器和处理器来说一定是自由度越大性能越好的,但是对于使用者来说就越难掌握的,因为我们不知道到底程序的执行顺序是怎样的了。已经不按照我们的逻辑顺序走了。
JMM对重排序有最基本的一个限制,as-if-serial:不管怎么重排序(单线程,多线程需要编写正确的代码才可以确保),程序的执行结果不能被改变
为了使开发者更好的理解内存可见性,JMM提出了happens-before
的概念来阐述内存可见性
与程序员密切相关的happens-before规则如下。
·程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
·监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
·volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的 读。
·传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
·start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的 ThreadB.start()操作happens-before于线程B中的任意操作。
·join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作 happens-before于线程A从ThreadB.join()操作成功返回。
这些规则就是并发编程可见性的规则,只有遵循了这些规则才不会出现可见性问题,否则JMM不保证线程间的可见性。
可以看到这里提到了锁,也就是锁本身除了对临界区进行排他性限制操作的特性外,它还是具有可见性的控制的。
在这里还可以看到volatile的内存语义对应锁的内存语义具有一些相同效果,写操作对应锁的释放,读操作对应锁的获取
实际上JMM更像是在中间定义了一套标准,向上是对程序员的规则,向下是输出统一的JMM定义的JVM字节码,针对不同JVM在实现时对重排序的限制做到性能和可编程性的平衡
这样我们只用理解了happens-before原则就可以写出没有内存可见性的并发代码,在JMM实现部分会根据这些规则插入内存屏障来限制重排序以及对本地内存与主内存的数据同步确保可见性
这个图更好的说明了JMM所处的位置既要保证足够的内存可见性,也要保证编译器和处理器根据自身的特性最大能力的优化性能
理解volatile
volatile主要的作用在线程可见性上,保持多线程之间对同一对象的获取/修改的可见性.
其次它还有对自身操作的原子性
锁也是依据volatile和CAS操作来实现获取的.它是并发包实现的基石,后面会展开说这里。
其他
有时候我们写代码的时候并没有清楚什么时候多线程就介入了。那这个时候你就还没理清楚,这都没弄清楚,那考虑多线程的问题基本就是瞎扯了。
所以从最基础的入手理清楚线程从哪里开始的分发,那里线程之间产生了依赖,怎样共享了数据,只有弄清楚了最基本的信息,才可以依托于理解的多线程知识解决实际的问题,否则无异于瞎蒙。