分为CPU内部和外部
会出现缓存一致性问题。由于各个CPU有了自己的缓存,从主存中读取数据到自己的缓存中更新数据,再刷回主存。在多线程情况下会产生缓存一致性问题。
int a=5; public int sum(){ return a+=5; }
例如线程1和线程2执行sum方法,操作都是将主存中的a=5分别+5,线程1读取后更新为a=10,还没刷回主存,此时线程2读取到主存中a=5,将a更新为10,之后全部更新到主存。此时a=10。
缓存一致性协议是一种缓存锁,目的是为了解决使用缓存后带来的可见性和一致性问题。MESI协议只是协议其中的一种,Intel使用的缓存一致性协议就是MESI。一下以缓存行为单位,每个单位64字节,cpu多个核可以共同操作缓存。
Modified: 表示该缓存行中的内容被修改了,与主存数据不一样,并且该缓存行只被缓存在该CPU中 。 该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取申请主存中相应内存之前)写回(write back)主存。 当被写回主存之后,该缓存行的状态会变成独享(exclusive)状态。
Excluslve: 独享,该缓存行只被缓存在该CPU的缓存中,它是未被修改过的,与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成共享状态(shared)。 同样的, ,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。
Shared: 该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致,当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废(变成无效状态(Invalid))。
Invalid:标记该缓存行被其他cpu修改过, 表示该缓存行中的内容时是无效的。 比如被Modified修改过。
会出现伪共享的问题,因为读取缓存时以缓存行(Cache Line)为单位,每个缓存行64字节。例如:有两个变量a和b在同一个缓存行中,有两个线程,线程1会修改a的值,线程b修改b的值。当线程1读取变量a时,会将b一起读取到cpu缓存中(因为是以缓存行为单位),线程1修改a值后,其他包含这个缓存行都会失效(标记为i)。当线程b想要修改b的值时,发现b所在的缓存行被表示为失效,需要重新去主存中读取。做这种无用功操作就是伪共享问题。
1.使用缓存对其
2.@Sum.misc.Contended 注解(JDK8) (-XX:RestrictContended)
注解可以使用在类上,也可以使用在变量上。
原子性、有序性、一致性
原子性:就是一个操作或多个操作中,要么全部执行,要么全部不执行。
例如:账户A向账户B转账1000元,这个么过程涉及到两个操作,(1)A账户减去1000元 (2)B账户增加1000元。这么两个操作必须具备原子性。否则A账户钱少了,B账户没增加。
有序性: 程序执行顺序按照代码先后顺序执行。
处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致(指令重排),但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。(此处的结果一致指的是在单线程情况下)
指令重排的理解:单线程侠,如果两个操作更换位置后,对后续操作结果没有影响,可以对这两个操作可以互换顺序。
可见性: 可见性是指多线程共享一个变量,其中一个线程改变了变量值,其他线程能够立即看到修改的值。
例如:
//线程1执行的代码 int i = 0; i = 10; //线程2执行的代码 j = i;
CPU1执行线程1代码,CPU执行线程2代码。CPU读取i=0到CPU缓存中,修改i=10到自己缓存,还没更新到主存,此时CPU2读取的i还是主存中i=0,此时j会被赋值为0;
在JVM内部,JMM把内存分为两部分:线程栈区和堆区。JVM运行过程中,每个线程都有自己的线程栈,线程栈包含了线程执行的方法相关信息,我们称为调用栈,是线程私有的。堆主要存储的是对象,线程共享的。
加volatile关键字,volatile可以保证可见性和有序性。
volatile修饰的共享变量在执行写操作后,会立即刷回到主存,以供其它线程读取到最新的记录。
volatile关键字底层通过lock前缀指令,进行缓存一致性的缓存锁定方案,通过总线嗅探和MESI协议来保证多核缓存的一致性问题,保证多个线程读取到最新内容。 lock前缀指令除了具有缓存锁定这样的原子操作,它还具有类似内存屏障的功能,能够保证指令重排的问题。
重排序分为编译器优化的重排序、指令级并行的重排序和内存系统的重排序。但是指令重排有一个规则,as-if-seiral:不管怎么重排序,单线程的程序执行结果不能够被改变,编译器、处理器等都得遵循这个规范和准则。
可以看见,上面赋值输出,不管什么情都不会输出0,0.问题,但是最后0,0还是出现了,为什么?因为出现了重排序优化(重排序就是某条指令优先执行),避免重排序需要用到Volatile优化。
public class Disorder {
private static int x = 0, y = 0;
private static int a = 0, b =0;
public static void main(String[] args) throws InterruptedException {
int i = 0;
for(;;) {
i++;
x = 0; y = 0;
a = 0; b = 0;
Thread one = new Thread(new Runnable() {
public void run() {
//由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
//shortWait(100000);
a = 1;//a=1
x = b;//1可能是1或0
}
});
Thread other = new Thread(new Runnable() {
public void run() {
b = 1;//b=1
y = a;//a可能是0或1
}
});
one.start();other.start();
one.join();other.join();
String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
System.err.println(result);
break;
} else {
//System.out.println(result);
}
}
}
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}
}
x和y都等于0情况分析
这里处理器 A 和处理器 B 可以同时把共享变量写入自己的写缓冲区(A1,B1),然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓存区中保存的脏数据刷新到内存中(A3,B3)。当以这种时序执行时,程序就可以得到 x = y = 0 的结果。
Java 中 volatile 关键字是一个类型修饰符。JDK 1.5 之后,对其语义进行了增强
Volatile关键字(JMM内存屏障),内存屏障也成为内存栏杆,是一个CPU指令,volatile修饰的变量,在读写操作前后都会进行屏障的插入来保证执行的顺序不被编译器等优化器锁重排序。
内存屏障的功能有两个:(1)阻止屏障两边的指令重排、(2)刷新处理器缓存(保证内存可见性)。
其实JVM是屏蔽了不同处理器架构的差异,提供了统一化的内存屏障,在CPU硬件层面不同处理器架构有不同的内存屏障,例如X86架构的内存屏障有4种: ifence、sfence、mfence、lock前缀指令。
cpu层面:使用cpu内存屏障(硬件)
SR-133 提出了 happens-before 的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。 与程序员密切相关的 happens-before 规则如下:
注意,两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。happens- before 的定义很微妙