线程安全问题线程不安全问题指的是一个类在多线程情况下运行时会出现一些未知的结果。线程安全问题主要包括:原子性、可见性、有序性、原子性。变量访问的操作除了执行操作的线程之外,离不开线程。那么这个操作就叫做原子操作,我们称之为原子操作。原子性问题主要有两大要素(共享变量+多线程)原子操作是针对共享变量的操作。局部变量是不是原子的并不重要(因为局部变量位于栈帧和线程内部,不存在多线程问题)。原子操作用于多线程。在线程环境下,单线程不存在线程安全问题。原子操作“不可分割”的含义描述对于共享变量的操作,对于除操作线程之外的其他线程,要么尚未发生,要么已经结束,它们看不到操作的中间结果。不能交错访问同一组共享变量。实现原子操作有两种方式:1.锁2.处理器的CAS指令锁一般是在软件层面实现的,而CAS通常是在硬件层面实现的实现long的两种基本类型的写操作Java语言中的/double不是原子的,其他六种基本类型都是原子的。使用volatile关键字修饰long/double类型可以使其成为原子的。可见在多线程环境下,当一个线程更新一个共享变量时,后续访问该共享变量的线程无法立即获取到更新后的结果,甚至永远获取不到结果。这种现象称为可见。性问题。处理器和内存的读写操作不是直接进行的,而是通过寄存器写缓冲区缓存和失效队列等组件来进行内存的读写操作。cpu==>写缓冲区==>缓存==>使队列无效||||||===========CacheCoherencyProtocolCacheSynchronization:虽然一个处理器的缓存内容不能被另一个处理器读取,但是一个处理器可以通过缓存一致性协议(MESI)读取其他处理器的缓存),并将读取到的内容更新到自己的缓存中。这个过程称为缓存同步。可见性问题的原因程序中的共享变量可能分配到处理器的寄存器中存储。每个处理器都有自己的寄存器,寄存器的内容不能被其他处理器访问。所以当两个线程被分配给不同的处理器并且共享变量存储在它们自己的寄存器中时,导致一个线程永远无法访问另一个线程的共享变量。大量更新会造成可见性问题。即使共享变量分配到主存中存储,处理器也是通过缓存读取主存的。当处理器A完成对共享变量的操作后,先将结果更新到缓存中。通过writebuffer,当运算结果只更新到writebuffer时,processorB访问共享变量,也会有可见性问题(writebuffer不能被其他processor访问)。共享变量的操作结果从缓存更新到另一个处理器的缓存后,但是被这个处理器放入了失效队列,处理器读取的共享变量的内容还是过时的,这也会导致可见性问题。.可见性保证是通过刷新处理器高速缓存来实现的:当处理器更新共享变量时,它必须将其更新最终写入高速缓存或主内存。刷新处理器缓存:当处理器操作一个共享变量时,其他处理器之前已经更新过共享变量,所以缓存或主存必须进行缓存和同步。volatile的作用是提示JIT编译器这个volatile修饰的变量可能被多个Thread共享使用,避免JIT编译器优化导致程序运行异常。读取volatile修饰变量时,先刷新处理器缓存操作,更新volatile修饰变量后刷新处理器缓存。单处理器会不会有可见性问题?单处理器实现多线程操作时,是通过上下文切换来实现的。当发生切换时,寄存器中的数据也会被保存下来,不会被“下面”访问到,所以当共享变量存储在寄存器中时,也会出现可见性问题。顺序重排序的概念:处理器执行操作的顺序与我们的目标代码指定的顺序不一致重排序有以下几种情况:编译器编译出的字节码的顺序与目标代码的执行顺序不一致bytecodeinstructionsisinconsistentwiththeobjectcode目标代码执行正确,但其他处理器对目标代码的执行顺序有错误在处理器B看来处理器A先执行操作b,这是一种感知错误。从重排序的来源来看,重排序一般分为:指令重排序和存储子系统重排序。重新排序是一种内存访问操作。一个不会影响程序在单线程下运行的正确性,但是会影响程序在多线程下运行的正确性的优化。出于性能考虑,指令重排序编译器在不影响程序正确性的情况下,对指令进行重排序,相应调整java程序的执行顺序,导致执行顺序与源代码顺序不一致。java平台有两种编译器:静态编译器(javac),将java源代码翻译成字节码文件(.class),在这个时期基本不发原始指令的重新排序。动态编译器(JIT)将java字节码动态编译成机器码,这期间经常发生指令重排序。现代处理器为了执行效率,往往不按程序顺序执行指令,而是动态调整指令执行的顺序,哪条指令就绪后先执行,这称为乱序执行。这些指令的执行结果在被写入寄存器或主存之前将被存储在重排序缓冲区中。区,然后重排序缓冲区会按照程序顺序将指令执行结果提交到寄存器或主存,因此乱序执行不会影响单线程执行结果的正确性,但有在多线程环境下会出现意想不到的结果。存储子系统重新排序(内存重新排序)Processor-0Processor-1data=1;//S1准备就绪=真;//S2while(!ready){}//L3System.out.println(data);//L4当Processor-0和Processor-1没有指令重排序时,Processor-0按照S1-S2的顺序执行程序,但是Processor-1首先感知到S2被执行,所以Processor-1有可能即L3-L4会在没有感应到S1的情况下执行,那么此时程序会打印出data=0,存在线程安全问题。上面的情况是S1和S2之间发生了内存重排序。看起来串行语义Reordering并不是编译器和处理器随机调整指令和内存操作结果的顺序,而是遵循一定的规则。遵循这个规则的编译器和处理器会给单线程程序带来一种顺序执行。错觉”,这种错觉称为似串行语义。为了保证似串行语义,有数据依赖的语句不会重新排序,没有数据依赖的语句可能会重新排序。以下面为例:语句③依赖语句①和语句②,所以不能重排,但是语句①和语句②没有数据依赖,所以语句①和语句②可以重排floatprice=59.0f;//语句①shortquantity=5;//语句②floatsubTotal=price*quantity;//语句③有控制依赖的语句可以重新排序,如下:flag和count有控制依赖,可以重新排序,即在不知道flag的值的情况下,为了追求效率,可以执行count++.if(flag){count++;}单处理器系统是否会重排1、静态编译期重新排序会影响单处理器系统Processor-0Processor-1data=1的处理结果;//S1准备就绪=真;//S2while(!ready){}//L3System.out.println(data);//L4在编译时按S1和S2排序如上图Processor-0Processor-1ready=true;//S2data=1;//S1while(!ready){}//L3System.out.println(data);//L4执行完S2后,程序进行从Processor-0到Processor-1的上下文切换。显然,这种重新排序导致了意想不到的结果和线程安全问题。2.RunPeriodicreordering(JIT动态编译,内存重排序)不会影响单处理系统的处理结果。当这些重新排序发生时,相关的指令还没有完全执行,系统不会进行上下文切换,会等到重新排序发生的指令执行并提交后,再进行上下文切换。因此,一个线程中的重新排序对切换后的另一个线程没有影响。上下文切换和上下文切换的直接开销包括:操作系统保存和恢复上下文所需的开销,主要是处理器时间开销。线程调度器的线程调度开销(例如,根据一定的规则确定哪个线程将占用处理器运行)。间接开销包括:处理器高速缓存重新加载开销被切换出去的线程可能会被切换进来,以便稍后在另一个处理器上继续运行。由于处理器之前可能没有运行过线程,线程在继续运行过程中需要访问的变量仍然需要处理器从主存或者其他处理器通过缓存一致性协议重新加载到缓存中。中间。这需要一定的时间。上下文切换也可能导致整个一级缓存的内容被刷新(Flush),即一级缓存的内容会被写入下一级缓存*(如二级缓存高速缓存)或主存储器(RAM)。
