我们都知道随着祖国的日益繁荣,随着科技的进步,设备的更新换代,计算机体系结构、操作系统、编译器都在不断改革创新,但是有总是有一点是不变的:那就是下面三者的性能和耗时:CPUCache<————>内存用一张图来表示(多核):我们下面的解释,如无特别说明,均以多核为准。02原因分析可见性问题是由于并发编程的CPU缓存不一致造成的,主要有以下三种情况:2.1.线程交叉执行线程交叉执行多是由于线程切换引起的,例如下图中线程A在执行过程中切换到线程B,然后再切换回线程A执行剩余的操作;此时线程B对变量的修改不能立即被线程A看到,从而导致计算结果不一致和理想结果的情况。2.2.重排序结合线程交叉执行比如下面的代码inta=0;//line1intb=0;//line2a=b+10;//line3b=a+9;//line4ifline1andIf编译时改变第2行的顺序,不影响执行结果;如果mutation中第3行和第4行的顺序互换,会影响执行结果,因为b的值小于预期的19;如图:由于编译时执行顺序发生变化,导致结果不一致;而且两个线程交叉执行导致换线程后的结果不是预期值,更糟糕!2.3.共享变量的更新值在工作内存和主内存中没有及时更新是因为主线程对共享变量的修改没有及时更新,无法在工作内存中立即获取到最新的值子线程,导致程序无法按照预期的结果执行。例如下面的代码:packagecom.itquan.service.share.resources.controller;importjava.time.LocalDateTime;/***@author:mmzsblog*@description:线程间共享变量的可见性测试*/publicclassVisibilityDemo{//状态标志flagprivatestaticbooleanflag=true;publicstaticvoidmain(String[]args)throwsInterruptedException{System.out.println(LocalDateTime.now()+"主线程开始计数子线程");newCountThread().start();Thread.sleep(1000);//设置flag为false,让上面启动的子线程跳出while循环,结束运行VisibilityDemo.flag=false;System.out.println(LocalDateTime.now()+"主线程将状态标志设置为false");}staticclassCountThreadextendsThread{@Overridepublicvoidrun(){System.out.println(LocalDateTime.now()+"countingchildthreadstartcount");inti=0;while(VisibilityDemo.flag){i++;}系统。out.println(LocalDateTime.now()+"count子线程结束计数,运行结束:i的值为"+i);}}}运行结果为:从打印结果可以看出console的,因为主线程是flag的修改不会立即被计数子线程看到,所以计数子线程不能跳出while循环结束子线程很长时间。对于这种情况,作为一个有强迫症的粉丝,当然不能忍,于是引出下一个问题:如何解决线程间不可见03如何解决线程间不可见为了保证线程之间的可见性,我们一般有3种选择:3.1、volatile:只保证可见性volatile关键字可以保证可见性,但也只能保证可见性。这里可以保证flag的修改可以立即被计数子线程获取到。这时候要纠正上面例子中的问题,只要在定义全局变量的时候加上volatile关键字//stateflagprivatestaticvolatilebooleanflag=true;3.2、Atomic相关类:保证可见性和原子性会标记状态flag如果在定义的时候使用Atomic相关类来定义,可以很好的保证flag属性的可见性和原子性。这时,要纠正上面例子中的问题,只需要在定义全局变量时,将变量定义为Atomic相关的类即可。//状态标志flagprivatestaticAtomicBooleanflag=newAtomicBoolean(true);设置新值和获取值的相关方法有一些变化,如下://设置flag的值VisibilityDemo.flag.set(false);//获取flag的值VisibilityDemo.flag.get()3.3、Lock:保证可见性和原子性这里我们使用Java中常见的synchronized关键字。这时候,为了更正上面例子中的问题,只需要在计数操作i++中加上synchronized关键字来修饰synchronized(this){i++;}通过以上三种方法,我得到了类似下面的预期结果:然而,接下来阿粉,我想对volatile和synchronized关键字做一个更详细的解释。04Visibility-volatileJava内存模型为volatile关键字定义了一些特殊的访问规则。当一个变量被volatile修饰时,它会有两个特点,或者说volatile有以下两层语义:第一,保证不同线程读取这个变量时的可见性。也就是说,一个线程修改了一个变量的值,这个新值立即对其他线程可见。(volatile解决了线程间共享变量的可见性问题)。其次,禁止指令重新排序以防止编译器优化代码。对于第一点,volatile在读取这个变量时保证了不同线程的可见性,具体如下:1:使用volatile关键字会强制一个线程修改的共享变量的值立即写入主存。2:如果使用volatile关键字,当线程2修改时,变量在线程1的工作内存中的cacheline会失效(反映到硬件层,即L1或L2缓存中对应的cachelineCPU无效);附一张CPU缓存模型图:3:由于变量在线程1工作内存中的缓存行无效,当线程1再次读取该变量的值时,会去主内存读取。基于此,我们经常在文章或书籍中看到volatile可以保证可见性。总结一下:就是一个用volatile修饰的变量,这个变量的读写不能使用CPU缓存,必须从内存中读取或者写入。使用volatile并不能保证线程安全,那么volatile有什么作用呢?其中之一:(标记状态量,保证其他线程看到的状态量是最新值)volatile关键字是Java虚拟机提供的最轻量级同步机制,很多人更愿意用synchronized来做synchronization,因为他们还不够了解(其实想了解的话可以看看happens-before原理)。那么接下来阿粉我来说说synchronized关键字。05可见性synchronized5.1、scopesynchronized关键字有两个作用域:1)在一个对象实例中,synchronizedaMethod(){}可以防止多个线程同时访问这个对象的synchronized方法。如果一个对象有多个synchronized方法,只要一个线程访问其中一个synchronized方法,其他线程就不能同时访问这个对象中的任何一个synchronized方法。此时,不同对象实例的synchronized方法互不干扰。也就是说,其他线程仍然可以同时访问同一个类的另一个对象实例中的synchronized方法。因为在修改非静态方法时,当前实例对象是被锁定的。2)是某个类的作用域,synchronizedstaticaStaticMethod{}防止多个线程同时访问该类中的synchronized静态方法。它适用于该类的所有对象实例。因为在修改静态方法的时候,锁定的是当前类的Class对象。5.2.可以在方法中的某个块中使用除了在方法前使用synchronized关键字外,synchronized关键字也可以在方法中的某个块中使用,表示只有这个块的资源是互斥的。用法是:synchronized(this){/*block*/}它的作用域是当前对象;5.3、cannotinheritsynchronized关键字不能继承,也就是说基类方法synchronizedf(){//SpecificOperation}在继承类中不会自动变成synchronizedf(){//specificoperation}而是变成f(){//specificoperation}被继承的类需要你显式指定它的其中一个方法为synchronized方法;总结以上3点:synchronized关键字主要有以下三种用法:修饰实例方法:作用于当前实例加锁,在进入同步代码前需要获取当前实例的锁修饰静态方法:作用在当前类对象上加锁,在进入同步代码之前,必须获取当前类对象的锁修改代码块:指定锁对象,锁定给定对象,进入同步前获取给定对象的锁代码块。这三种用法基本保证了共享变量读取的时候,读取的是最新的值。5.4.JVM对synchronized的两条规定:在线程解锁之前,必须将共享变量的最新值刷新到主存中。当线程被锁定时,工作内存中共享变量的值将被清除。重新读取内存中的最新值(注意:上锁和解锁是同一个锁)。从上面两条规则也可以看出,这种方式保证了内存中的共享变量一定是最新的值。但是我们在使用synchronized的时候也要注意以下几点来保证可见性:A.不管synchronized关键字是加在方法上还是对象上,其获取的锁都是对象;而不是使用一段代码或函数作为锁————而且同步方法很可能被其他线程的对象访问。B.每个对象只有一个与之关联的锁(lock)。Java编译器会自动在synchronized修饰的方法或代码块前后加上lock()和unlock()。这样做的好处是lock()和unlock()必须成对出现,毕竟忘记解锁unlock()是一个致命的bug(意味着其他线程只能死去等待)。C、实现同步需要大量的系统开销作为代价,甚至可能造成死锁,所以尽量避免不必要的同步控制。以上内容是我对合并方法中可见性的理解和总结。下一期,我们继续讲述并发中的有序性。参考文章:1.极客时代Java并发编程2.https://www.jianshu.com/p/89a8fa8ffe393。https://www.cnblogs.com/xiaonantianmen/p/9970368.html4。https://www.lagou.com/lgeduarticle/78722.html5、https://blog.csdn.net/evankaka/article/details/441537096、https://juejin.im/post/5d52abd1e51d4561e6237124