Java并发编程-Java内存模型(JMM)前言在上一章Java并发编程-AndroidUI框架为什么是单线程的?在文章中,作者介绍了并发编程线程安全的“三害”:“可见性”、“原子性”和“有序性”。于是,第一个“bigevil-visibility”顾名思义,就是线程B能否看到线程A对共享变量的操作结果。第二,“双恶原子”CPU指令/操作被中断/分割。从广义上来说,笔者认为,也是一个可见性问题。比如当线程A修改共享变量x+=1时,被中断,执行线程Bx=40。当线程A恢复执行时,可能看不到x已经被修改为40。最后,“三害——orderliness”表示程序按照代码的顺序执行,即先执行x=40,x==40必须在后面读取。如果有各种优化导致指令重排序,则可能不正确whenx==40后面再看。总而言之,并发编程的大部分问题都是“可见性”问题,在上一章Java内存模型(JMM)的作者介绍,“可见性”的原因是缓存,而“有序”的原因是为了各种优化(编译时、处理器、运行时),我首先想到的是禁用缓存,禁止各种优化操作。Java中如何禁用缓存,禁止各种优化操作?Happens-BeforeJava内存模型由操作定义,包括对变量的读/写操作、监视器锁的获取和释放以及线程的启动和合并。JMM为程序中的所有操作定义了一个偏序关系2,称为Happens-Before。为了保证执行操作B的线程看到操作A的结果(不管A和B是否在同一个线程中执行),A和B之间必须满足Happens-Before关系。如果没有Happens-Before关系在两个操作之间,JVM可以任意重新排序它们。Happens-Before的规则包括:规则一:程序顺序这个规则比较符合我们的直觉。单线程中代码的顺序就是程序的执行顺序:intx=40会先执行inty=41publicvoidtest(){intx=40;inty=41;}规则二:monitorlock3monitorlock是内置锁(synchronized),对monitorlock的unlock操作必须在同一个monitorlock的lock操作之前执行,这个规则很容易理解,现实世界中的锁在关闭锁之前必须处于打开状态规则3:volatile变量45对volatile变量的写操作必须在对该变量的读操作之前执行。这个规则不容易理解。以下代码和文字摘自深入理解Java内存模型(四)——volatile了解volatile特性的一个好方法就是把对volatile变量的单次读/写看作是使用同一个监视器锁同步这些单一的读/写操作6classVolatileFeaturesExample{volatilelongvl=0L;//使用volatile声明一个64位long变量publicvoidset(longl){vl=l;//写一个单一的volatile变量}publicvoidgetAndIncrement(){vl++;//读/写复合(多个)volatile变量}publiclongget(){returnvl;//读取单个volatile变量}}假设有多个线程分别调用上面程序的三个方法,这个程序在语义上等价于下面的程序:classVolatileFeaturesExample{longvl=0L;//64位long型普通变量publicsynchronizedvoidset(longl){//对于单个普通变量Write与同一个监视器同步vl=l;}publicvoidgetAndIncrement(){//普通方法调用longtemp=get();//调用同步读取方法temp+=1L;//正常写操作set(temp);//调用同步写入方法}publicsynchronizedlongget(){returnvl;//单个普通变量的读取与同一个monitor同步}}如上面的示例程序所示,对于一个volatile变量单个读/写操作,一对一两个普通变量的读/写操作使用同一个监视器锁同步,它们之间的执行效果是一样的。从内存语义上看,volatile和monitorlock的作用是一样的:volatilewrite和monitorrelease是一样的。相同的内存语义;易失性读取与监视器读取具有相同的内存语义。简而言之,volatile变量本身具有以下属性:可见性。读取volatile变量总是会看到(由任何线程)对volatile变量的最后一次写入。原子性:读取/写入任何单个volatile变量是原子的,但像volatile++这样的复合操作不是原子的。规则四:线程启动在线程中执行任何操作之前,必须先执行线程上对Thread.start的调用。这个规则很好理解,就是先执行线程A的启动操作,再执行线程A中的操作。也就是说,线程A启动前的操作结果对线程A中的操作是可见的。参考下面的示例代码:Threadthread=newThread(()->{//线程中的操作,可以看到结果线程启动前对共享变量的操作System.out.println(x);});//在线程启动前操作共享变量x=10;//线程启动thread.start();规则5:线程结束线程中的任何操作必须在其他线程检测到线程已经结束之前执行,或者从Thread.join成功返回,或者调用Thread.isAlive返回false。这个规则也很好理解,就是在线程结束之前,线程中的任何操作都已经执行完毕。也就是说,线程结束后,线程中的操作结果对后续操作是可见的。参考下面的示例代码:Threadthread=newThread(()->{//线程中的操作,可以看到线程启动前对共享变量的操作结果System.out.println(x);//操作线程中的共享变量x=20;});//线程启动前操作共享变量x=10;//线程启动thread.start();//线程加入,等待线程结束thread.join();//在线程中对共享变量的操作结果可以在System.out中看到这里。打印(x);规则6:线程中断当一个线程在另一个线程上调用中断时,必须在被中断的线程检测到中断调用之前执行(通过抛出InterruptedException,或调用isInterrupted和interrupted)。这个规则我不是很理解,只是理解了表面意思:线程A对线程B中断的调用必须在线程B检测到中断事件之前执行。规则七:终结器对象的构造函数必须在启动该对象的终结器之前完成。这个规则很好理解,可以理解为AndroidActivity的生命周期。这里说的constructor和finalizer就是对象的生命周期。在调用终结器之前,对象必须已经初始化。规则八:传递性如果操作A先于操作B执行,操作B先于操作C执行,则操作A必须先于操作C执行。看到这条规则,笔者首先想到的是《秦时明月》中的《白马非马》辩论:白马==传家宝,黑马==传家宝,传家宝==传家宝,白马==黑马;这个规则可以理解为大小关系,比如:a大于b,b大于c,那么a一定大于c。传递规则可以与其他规则相结合,以达到意想不到的效果。总结1以上规则虽然只满足偏序关系,但是同步操作,比如锁的获取和释放操作,以及volatile变量的读写操作,都满足全序关系。因此,在描述Happens-Before关系时,可以使用“后续的锁获取操作”、“后续的volatile变量的读操作”等术语。Java中的final字段,无论是写类、变量还是方法,大部分程序员可能会忽略final关键字的作用,从而导致一些不可预见的问题。在Kotlin中,类和方法默认是final的,声明变量时建议使用val修饰,而不是var。Java中基于final关键字修饰的变量在多线程中也具有不可变的特性。建议优先使用final修饰变量。综上所述,Java内存模型是JVM的一种规范,其中Happens-Before定义了一些与程序员相关的规则。可以理解,Happens-Before规则是底层暴露给程序员的一些接口,程序员正确使用这些接口。可以写出正确的代码。Happens-Before规则可以说强制执行代码的某种执行顺序,同时表示后续操作可以看到上一次操作的结果了解Java内存模型有助于分析和解决遇到的线程安全问题以及理解线程安全问题的原因,重塑自己的编程思路。笔者认为最重要的是重塑自己的编程思维。俗话说“读卷千卷,写如神”,搞懂原理,掌握规律,重塑思维,才能在编码时写出“Bug”(狗头救命)。).Java并发编程真的很难,作者水平有限。如有错误,请不吝赐教。解释和参考《Java并发编程实战-第十六章》?偏序关系π是具有反对称、自反和传递性质的集合上的关系?在加锁和解锁等操作中,显示锁与内置锁具有相同的内存语义?原子变量和易变变量具有read/write操作中语义相同?Java内存模型FAQ(十)什么是volatile?深入理解Java内存模型(四)——volatile?就是finalize()方法?
