上一节阿粉和大家讨论了并发小人的可见性和原子性。本节我们将继续攻克三害之一的秩序。为什么要讨论顺序?因为Java是面向对象编程,只关注最终的结果,很少去研究它的具体执行过程?正如上一篇文章介绍可见性时所描述的那样,为了提高性能,操作系统在将Java语言转换为机器语言时,会指示编译器修改语句的执行顺序,从而达到优化系统性能的目的。因此在许多情况下,访问程序变量(对象实例字段、类静态字段和数组元素)的执行顺序可能与程序语义指定的顺序不同。众所周知,Java语言运行在Java自带的JVM(JavaVirtualMachine)环境中。在JVM环境下,源代码(.class)的执行顺序与程序(runtime)的执行顺序不一致,或者程序执行顺序与编译器的执行顺序不一致时,我们说发生重排序程序执行。编译器的修改是因为它认为可以保证最终的运行结果!因为在单核时代没问题;但是随着多核时代的到来,在多线程环境下,当线程切换发生时,这种优化会大大增加。发生事故的概率降低了!好心办坏事!也就是说,有序性是指在代码序列结构中,我们可以直观的指定代码的执行顺序,即从上到下依次执行。但是编译器和CPU处理器会根据自己的决定重新排列代码的执行顺序。优化指令的执行顺序,提高程序的性能和执行速度,改变语句的执行顺序,重新排序,但是最后的结果好像没有变化(单核)。顺序问题是指在多线程环境(多核)下,执行语句重排序后,这部分重排序没有一起执行,所以切换到其他线程,导致结果不一致。这就是编译器编译优化给并发编程带来的程序顺序问题。图为:阿芬总结:编译优化最终导致排序问题。1.有序性原因:如果一个线程向字段a写入一个值,然后向字段b写入一个值,并且b的值不依赖于a的值,那么处理器可以自由调整它们的执行顺序,并且缓冲区可以在a之前将b的值刷新到主内存。此时,可能会出现秩序问题。示例:1importjava.time.LocalDateTime;23/**4*@author:mmzsblog5*@description:并发中的有顺序问题6*@date:2020年2月26日15:22:057*/8publicclassOrderlyDemo{910staticintvalue=1;11privatestaticbooleanflag=false;1213publicstaticvoidmain(String[]args)throwsInterruptedException{14for(inti=0;i<199;i++){15value=1;16flag=false;17Threadthread1=newDisplayThread();18Threadthread2=newCountThread();19thread1.start();20thread2.start();21System.out.println("=========================================================");22Thread.sleep(6000);23}24}2526staticclassDisplayThreadextendsThread{27@Override28publicvoidrun(){29System.out.println(Thread.currentThread().getName()+"DisplayThreadbegin,time:"+LocalDateTime.now());30value=1024;31System.out.println(Thread.currentThread().getName()+"changeflag,time:"+LocalDateTime.now());32flag=true;33System.out.println(Thread.currentThread().getName()+"DisplayThreadend,time:"+LocalDateTime.now());34}35}3637staticclassCountThreadextendsThread{38@Override39publicvoidrun(){40if(flag){41System.out.println(Thread.currentThread().getName()+"valueis:"+value+",time:"+LocalDateTime.now());42System.out.println(Thread.currentThread().getName()+"CountThreadflagistrue,time:"+LocalDateTime.now());43}else{44System.out.println(Thread.currentThread().getName()的值+"的值是:"+value+",time:"+LocalDateTime.now());45System.out.println(Thread.currentThread().getName()+"CountThreadflagisfalse,time:"+LocalDateTime.now());46}47}48}49}运行结果:从打印结果可以看出:DisplayThread线程在执行时,必然有一次重新排序,导致先给flag赋值,然后切换到CountThread线程。如果打印值为1且falg值为true,则给value赋值;但这样做的原因是两个赋值语句之间没有联系,所以编译器在编译代码时可能会发出指令重新排序的示意图如下:2.如何解决排序问题2.1.volatile的底层使用了内存屏障来确保顺序(一种使一个CPU缓存中的状态(变量)对其他CPU缓存可见的方法)技术)。volatile变量有一个规则,它指的是对一个volatile变量的写操作,Happens-Before在后续对该volatile变量的读操作。而这个规则是传递性的,也就是说:这时候我们在定义变量flag的时候,使用volatile关键字对其进行修饰,如:1privatestaticvolatilebooleanflag=false;此时变量的含义如下:即只要读到flag=true;你可以阅读value=1024;否则,您可以阅读flag=false;和未修改的初始状态值=1;但也可能存在线程切换导致的原子性问题。就是flag=false的情况;读取值=1024;看过之前关于[atomicity]()的文章的朋友可能马上就明白这是线程切换造成的。2.2.加锁这里我们直接使用Java语言内置的关键字synchronized对可能重排的部分进行加锁,这样无论从宏观上还是执行结果上都不会出现重排的现象。代码修改也很简单,使用synchronized关键字修改run方法即可,代码如下:1publicsynchronizedvoidrun(){2value=1024;3flag=true;4}同样,既然是锁,当然可以也可以使用Lock来加锁,但是Lock需要用户手动释放锁。如果不主动释放锁,可能会导致死锁。这一点在使用的时候一定要注意!使用这种方法加锁也很简单,代码如下:1readWriteLock.writeLock().lock();2try{3value=1024;4flag=true;5}finally{6readWriteLock.writeLock().unlock();7}好了,以上内容是我对并发中有序性的理解和总结。通过这三篇文章,我们大致掌握了并发中常见的可见性和有序性。属性、原子性问题及其常见解决方案。最后,阿芬简单总结下三篇文章所用方案的区别:参考文献[1]:https://juejin.im/post/5d52abd1e51d4561e6237124[2]:https://juejin.im/post/5d89fd1bf265da03e71b3605[3]:https://www.cnblogs.com/54chensongxia/p/12120117.html[4]:http://ifeve.com/jmm-faq-reordering/
