实例:
两个线程同时执行的情况下,执行i++ 操作,i初始化为1,得到的结果并非为3,而是2.过程为:
线程1和2 同时从主存中获取i 的值并写入缓存,在分别执行+1 操作,之后再分别写入主存,这时出现了最终主存的值为2的情况。这个是因为线程内操作对其他线程不可见,解决缓存一致性的方案有:
-
通过在总线加LOCK#锁的方式;
-
通过缓存一致性协议。
但是方案1的缺点是总线加锁会出现阻塞的情况,效率低。
方案2的核心思想是:缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,因此其他CPU在读取该变量时,发现其无效会重新从主存中加载数据。
多线程并发的特点来源于:可见性,原子性和有序性
特点:
1、volatile 无法保证原子性操作(可理解为指令级别,java 对基本的数据类型的赋值操作进行了原子性操作的保证。但long和double 不是),
2、volatile 可保证可见性:当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,当其他线程读取共享变量时,它会直接从主内存中读取。当然,synchronize和锁都可以保证可见性。
3、在Java内存模型中,为了效率是允许编译器和处理器对指令进行重排序,当然重排序它不会影响单线程的运行结果,但是对多线程会有影响。Java提供volatile来保证一定的有序性。如单例模式中的双重检查
public class Singleton {
private static Singleton instance = null;
//private volatile static Singleton instance = null;
public static Singleton getInstance() {
if(null == instance) { // 线程二检测到instance不为空
synchronized (Singleton.class) {
if(null == instance) {
instance = new Singleton(); // 线程一被指令重排,先执行了赋值,但还没执行完构造函数(即未完成初始化)
}
}
}
return instance; // 后面线程二执行时将引发:对象尚未初始化错误
}
}
:假设线程一执行到instance = new Singleton()这句,这里看起来是一句话,但实际上其被编译后在JVM执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情:
1)给instance实例分配内存;
2)初始化instance的构造器;
3)将instance对象指向分配的内存空间(注意到这步时instance就非null了)
如果指令按照顺序执行倒也无妨,但JVM为了优化指令,提高程序运行效率,允许指令重排序。如此,在程序真正运行时以上指令执行顺序可能是这样的:
a)给instance实例分配内存;
b)将instance对象指向分配的内存空间;
c)初始化instance的构造器;
这时候,当线程一执行b)完毕,在执行c)之前,被切换到线程二上,这时候instance判断为非空,此时线程二直接来到return instance语句,拿走instance然后使用,接着就顺理成章地报错(对象尚未初始化)
具体来说就是synchronized虽然保证了线程的原子性(即synchronized块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)。
根据以上分析可知,解决这个问题的方法是:禁止指令重排序优化,即使用volatile变量
volatile 原理:
volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。
上面那段话,有两层语义:
-
保证可见性、不保证原子性;
-
禁止指令重排序。
Happen-before 原则:
其定义如下:
-
同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说,这一是规则无法保证编译重排和指令重排)。
-
监视器上的解锁操作 happen-before 其后续的加锁操作。(Synchronized 规则)
-
对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则)
-
线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则)
-
线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。
-
如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。
volatile经常用于两个场景:状态标记、double check