并发编程的三个特性原子性的一个操作或多个操作,或所有操作都已执行且不受任何因素的干扰而中断,或要么所有的操作都执行,要么都不执行
关于基本数据类型的访问,读写都是原子性的【long和double可能是例外】。
如果需要更大范围的原子性保证,可以使用synchronized关键字满足。
可见性当一个变量修改共享变量时,显示另外的线程都能立即看到修改后的最新值
volatile不仅可以保证共享变量的可见性,还可以在同步和最终中都实现可见性。
同步:在对变量执行unclock之前,必须将变量同步到主内存。
final :用构造函数初始化用final限定的字段,如果构造函数没有传递对this的引用,则final字段的值将显示在其他线程中。
有序性是按照代码的优先顺序执行程序的执行顺序。【指令重排序的存在使得Java在编译器和执行过程中优化输入代码,代码的执行顺序不一定按照编写代码的顺序
什么是CPU缓存型号? 计算机执行程序时,各指令由CPU执行,但执行指令时,数据的读取和写入程序执行中的**临时数据存储在主存**中,因此在这种情况下为http://因此,如果随时通过与内存的交互进行对数据的操作,指令的执行速度会大幅降低。
为了解决处理器速度和内存不匹配的问题,出现了CPU Cache。
图源: Java指南
缓存一致性问题在程序运行时为由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,CPU计算时为将运算需要的数据从主存复制一份到CPU的高速缓存当中,计算结束后,缓存中的数据为http://www.com
在单线程上运行没有任何问题,但在多线程环境中会出现问题。 举个简单的例子,有以下代码。
i=i 1; 根据以上分析,主要分为以下几个步骤。
从主内存读取I的值,并复制到缓存中。 CPU执行I加1的操作,并将数据写入缓存。 运算结束后,将缓存中的数据刷新到内存中。 在直接从它的高速缓存读取数据和向其中写入数据上,可能会发生什么现象?
在初始情况下,两个线程分别读取I的值,并将其存储在各自所在的CPU缓存中。 线程T1进行递增操作,并将I的最新值1写入存储器。 此时,线程T2的缓存内的I的值保持为0,进行递增操作,将I的最新值1写入存储器。 最终结果是i=1而不是I=2,http://www.Sina.com /
解决缓存不一致性的方法解决缓存不一致性的方法通常有两种解决方案:【均在硬件级别提供】:
刷新到主存
在早期的CPU中,通过对总线施加LOCK#锁定来解决缓存不一致性。 由于CPU与其他部件之间的通信是通过总线进行的,因此对总线施加LOCK#锁定将阻止其他CPU对其他部件(如内存)的访问,并且只有一个CPU可以使用此变量的内存。 例如,在上述示例中,如果一个线程正在执行i=i 1,并且在该代码执行期间在总线上出现LCOK#锁定信号,则等待该代码完全执行,然后另一CPU从变量I所在的存储器读取该变量这解决了缓存不一致的问题。
但是,在锁定总线时,存在其他CPU无法访问内存且效率低下的问题,从而产生了以下缓存一致性协议:
多线程环境
有名的是英特尔MESI协议,MESI协议确保每个缓存中使用的共享变量的副本一致。
如果一个变量在多个CPU中都存在缓存(一般在多线程编程时才会出现),那么就可能存在缓存不一致的问题。,即其他CPU,也存在该变量的副本,并且发出通过在总线加LOCK#锁的方式的信号,因此,如果其他CPU需要读取该变量,则其缓存中会包含该变量的副本【缓存机制:每个处理器通过侦听总线上传播的数据来检查自己的缓存值
基于mesiconsistencyprotocol,每个处理器必须始终从主内存循环通过嗅探器和CAS,无效的交互会导致整体
线带宽达到峰值,出现总线风暴。 JMM内存模型是什么JMM【Java Memory Model】:Java内存模型,是java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别,以实现让Java程序在各种平台下都能达到一致的内存访问效果。
它描述了Java程序中各种变量【线程共享变量】的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
注意,为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。也就是说,在java内存模型中,也会存在缓存一致性问题和指令重排序的问题。
JMM的规定所有的共享变量都存储于主内存,这里所说的变量指的是【实例变量和类变量】,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。
每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。
每个线程不能访问其他线程的工作内存。
Java对三大特性的保证 原子性在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。
为了更好地理解上面这句话,可以看看下面这四个例子:
x = 10; //1y = x; //2x ++; //3x = x + 1; //4 只有语句1是原子性操作:直接将数值10赋值给x,也就是说线程执行这个语句的会直接将数值10写入到工作内存中。语句2实际包含两个操作:先去读取x的值,再将x的值写入工作内存,虽然两步分别都是原子操作,但是合起来就不能算作原子操作了。语句3和4表示:先读取x的值,进行加1操作,写入新的值。需要注意的点:
在32位平台下,对64位数据的读取和赋值是需要通过两个操作来完成的,不能保证其原子性。在目前64位JVM中,已经保证对64位数据的读取和赋值也是原子性操作了。Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。 可见性Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
有序性在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在Java里面,可以通过volatile关键字来保证有序性,另外也可以通过synchronized和Lock来保证有序性。
Java内存模型具备一些先天的有序性,前提是两个操作满足happens-before原则,摘自《深入理解Java虚拟机》:
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作【让程序看起来像是按照代码顺序执行,虚拟机只会对不存在数据依赖性的指令进行重排序,只能保证单线程中执行结果的正确性,多线程结果正确性却无法保证】锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
volatile解决的问题保证了不同线程对共享变量【类的成员变量,类的静态成员变量】进行操作是时的可见性,一个线程修改了某个变量的值,新值对其他线程来说是立即可见的。
禁止指令重排序。
举个简单的例子,看下面这段代码:
//线程1boolean volatile stop = false;while(!stop){ doSomething();}//线程2stop = true; 线程1和2各自都拥有自己的工作内存,线程1和线程2首先都会将stop变量的值拷贝一份放到自己的工作内存中,共享变量stop通过volatile修饰,线程2将stop的值改为true将会立即写入主内存。线程2写入主内存之后,导致线程1工作内存中缓存变量stop的缓存行无效。线程1的工作内存中缓存变量stop的缓存行无效,导致线程1会再次从主存中读取stop值。 volatile保证原子性吗?怎么解决?volatile无法保证原子性,如对一个volatile修饰的变量进行自增操作i ++,无法保证多线程下结果的正确性。
解决方法:
使用synchronized关键字或者Lock加锁,保证某个代码块 在同一时刻只能被一个线程执行。使用JUC包下的原子类,如AtomicInteger等。【Atomic利用CAS来实现原子操作】。 volatile的实现原理下面这段话摘自《深入理解Java虚拟机》:
观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令。
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;它会强制将对缓存的修改操作立即写入主存;如果是写操作,它会导致其他CPU中对应的缓存行无效。 volatile和synchronized的区别volatile变量读操作的性能消耗与普通变量几乎没有什么差别,但是写操作则会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。不过即便如此,大多数场景下volatile的总开销仍然要比锁来的低。
volatile只能用于变量,而synchronized可以修饰方法以及代码块。volatile能保证可见性,但是不能保证原子性。synchronized两者都能保证。如果只是对一个共享变量进行多个线程的赋值,而没有其他的操作,推荐使用volatile,它更加轻量级。volatile 关键字主要用于解决变量在多个线程之间的可见性,而 synchronized 关键字解决的是多个线程之间访问资源的同步性。 volatile的使用条件使用volatile必须具备两个条件【保证原子】:
对变量的写操作不依赖于当前值。该变量没有包含在具有其他变量的不变式中。 最后由于篇幅有限,这里就不一一罗列了,20道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档,点击这里免费下载
还有更多面试复习笔记分享如下
道常见面试题(含答案)+21条MySQL性能调优经验小编已整理成Word文档或PDF文档,点击这里免费下载**
[外链图片转存中…(img-EfIpxyep-1625675297939)]
还有更多面试复习笔记分享如下
[外链图片转存中…(img-s3uwSe7G-1625675297941)]