为什么线程会不安全-浅谈JMM

内存模型(Java Memory Model,简称 JMM )是定义了线程和主内存之间的抽象关系,即 JMM 定义了 JVM 在计算机内存(RAM)中的工作方式,如果我们要想深入了解Java并发编程,就要先理解好Java内存模型。

线程不安全多出现在多CPU架构的服务上,先上一张图
在这里插入图片描述
JVM在运行的时候,会开辟一片内存作为运行时内存使用,java中变量属性和RAM主内存不是直接关系。但是在使用时候会建立

1.

举个例子

Class App {
	App app = new App();
	
	new Thread(() -> {
	    app.exec();
	}, "t1").start();
	       
	public void exec() {
	    System.out.println("进入循环");
	    int i = 0;
	    while (flag) {
	        i++;
	    }
	    System.out.println("跳出循环" + i);
	}
}

1.11. flag就会从JVM中刷到主内存中
1.12 t1线程运行时候 就会去抢占CPU 占用时间片
1.13 从主内存中read flag属性 load到CPU 高速缓存区 CPU use高速缓存(read -> loada -> use)
1.14 从缓存区拿数据 执行CPU完成 线程逻辑
1.15 CPU执行完成后 会将数据assign到缓存区 再store 保存 write至主内存中(assign -> store -> write)

1.2

加上一段代码

	Thread.sleep(2000);
	
	new Thread(() -> {
	    app.refresh();
	}, "t2").start();
        
	public void refresh() {
        flag = false;
        System.out.println("修改了flag=false");
    }

休息2s后 执行refresh方法 刷新flag值 运行结果如下:
在这里插入图片描述
并没有结束循环,这是因为高速缓存区的特性(接下来着重说说这里)。那么为什么不直接从主内存中拿数据呢,因为CPU在运行的时候从高速缓存区和从主内存拿数据,效率差别是量级的。

1.3

我们将变量flag 加上一个volatile,这是再运行
在这里插入图片描述
这时候我们发现,运行结果如我们所期望,结束运行了。为什么加了volatile就可以正常跳出循环,不加volatile但是更改了flag值,为什么不跳出循环呢?

1.4 引入缓存一致性协议:

1.41 对一个在缓存中共享的变量进行写入,首先发送一个失效的消息给到其他缓存了该数据的 CPU。并且要等到他们的确认回执。CPU1 在这段时间内都会处于阻塞状态。
1.42 为了避免阻塞带来的资源浪费。在 cpu 中引入 了 Store Bufferes(存储缓存) 和 Invalidate Queue(无效队列),为了避免阻塞带来的资源浪费。在 cpu 中引入 了 Store Bufferes(存储缓存) 和 Invalidate Queue(无效队列)
1.43 CPU1 写入共享数据时,直接把数据写入到 store bufferes 中,同时发送 invalidate 消息,然后继续去处理其他指令
1.44 当收到其他所有 CPU 发送了 invalidate ACK消息时,再将 store bufferes 中的数据数据存储至 cache 中。最后再从本地Cache同步到主内存
1.45 引入了 Store Bufferes 后,处理器会先尝试从 Store Bufferes 中读取值,如果 Store Bufferes 中有数据,则直接从Store Bufferes 中读取,否则就再从本地Cache中读取,从Store Bufferes读取数据存在脏读(假设后面cpu1进行修改了a变量,当没有执行刷新失效,即其他cpu进行修改完成的时候,则cpu0的缓存数据与buffer数据会出现不一致,cpu0读取buffer数据)

1.5 缓存一致性协议

常见的协议有 MSI、MESI、MOSI 等。最常见的就是 MESI 协议:
1.51 M(Modify) 表示共享数据只缓存在当前 CPU 缓存中, 并且是被修改状态,也就是缓存的数据和主内存中的数据不一致
1.52 E(Exclusive)表示缓存的独占状态,数据只缓存在当前 CPU 缓存中,并且没有被修改
1.53 S(Share)表示数据可能被多个 CPU 缓存,并且各个缓存中的数据和主内存数据一致
1.54 I(Invalid)表示缓存已经失效

x=1,y=2 这个会在一个缓存行内 如果CPU1在计算x CPU2在计算y CPU1先计算完 CPU2的计算y就会被失效,也叫伪共享.
cat /proc/cpuinfo
查看CPU缓存行大小 cach_alignment
JDK8中@Contended注解自动填充一个缓存行
-XX:-RestrictContended开启填充缓存行

1.6 主内存的lock与unlock

当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个 LOCK# 信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把 CPU 和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,总线锁定的开销比较大。因为想要强一致性,导致CPU阻塞等待,得不偿失。

2.1 volatile

在java中 volatile修饰词,在之前理解比较浅的情况下,只知道内存可见,其实volatile在主内存中相当于一个触发器角色,触发高速缓存一致性协议,了解了缓存一致性协议 也就知道了volatile为什么内存可见了,多线程操作情况下还是不安全的了。

2.11 内存可见 变量直接从内存读取 CPU操作会刷回主内存
2.11 volatile内存屏障 保证有序性
2.12 阻止屏障两侧的指令重排序;
2.13 写屏障:强制把写缓冲区/高速缓存中的脏数据等写回主内存,读屏障:将缓冲区/高速缓存中相应的数据失效

看dubbo源码时,我们会发现有很多单例 变量用到了volatile,是因为当我们new一个对象时,申请内存 对象实例化 内存地址赋值,这是一个理想的情况下的执行流程,但是也存在一种第二步和第三步颠倒的情况,所以需要volatile防止指令重排

两个面试题:
happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见。
as-if-serial语义的意思是:不管如何重排序,(单线程)程序的执行结果不会改变。

猜你喜欢

转载自blog.csdn.net/weixin_45657738/article/details/109157050