由于CPU比内存以及磁盘的速度快得多,CPU操作一个变量需要等到内存、磁盘读取成功。这时的CPU会死等在这里,极大的影响了CPU利用率。为了解决这个问题、硬件工程师先是增加了高速缓存,事先让一些常用变量加载到高速缓存中,这样CPU就不需要等待内存加载成功,这样提高了CPU的利用率,但是体验还是不行。为了提升CPU的利用率,发明了线程,在CPU死等的时候去运行其他的内容,极大地利用了CPU资源。但是工程师没有满足,为了得到更快的运行效率,提出了多核CPU。本来在单核CPU的多线程环境中没有可见性问题,但是随着CPU从单核变为多核就出现了可见性问题。
有一个场景,有两个CPU核心,每个CPU核心有一个缓存。主内存保存有count=0;有两个线程对count进行循环加一操作。
当第一个线程得到CPU0的时间片时,对count进行加操作。即从0开始加。
而此时第二个线程恰好得到CPU1的时间片,也会进行count的加操作。也会从缓存里面读出值,此时CPU1缓存中的值还是0,并不是CPU0中已经计算到的值。就会进行从0开始加的操作。这个就是可见性问题。
其原因是因为不同CPU的缓存中的数据没有共享。、
因为CPU的缓存数据没有共享,才导致可见性问题,解决方法当然是让数据共享,即让不同CPU的缓存一致。可以通过缓存锁的的方式来保证缓存一致。即让CPU读写是遵循缓存一致性协议,比如MESI协议。该协议规定了缓存的四种状态。其中M、S、E状态下CPU可以从缓存中读取数据,当缓存处于I状态,CPU只能从主内存中读取数据。这样就可以保证缓存一致性。大致思路是如果一个CPU修改了值,他会告知其他CPU他缓存中的值现在已经无效了,需要从主内存里面重新加载,其他的CPU在接受到这个信息之后,会返回一个ACK给修改值的CPU。CPU接收到值之后才进行将数据同步到主内存,再接着运行其他指令,当然这个过程是阻塞的。为了解决阻塞问题,提高CPU的利用率,引入了一个称作StoreBuffer的东西,即CPU在通知其他CPU之后,就将该数据保存在StoreBuffer,自己去执行其他代码去了,不阻塞在这里。等到ACK返回之后,再将StoreBuffer中的数据同步到缓存以及主内存中。
但是,通过该方式还有一个问题没有解决,就是指令重排序的问题。上面讲到,为了最大化利用CPU资源,CPU死等其他CPU的ACK,它会先运行其他指令。这样就会打乱一些变量之间的逻辑关系。举个简单的例子。见下面代码。
package com.test.demo;
public class VolatileTest {
static boolean is_Finish =false;
static int i =0;
public static void main(String[] args) {
Thread thread = new Thread(() -> {
i = 10;
is_Finish = true;
});
Thread thread1 = new Thread(() -> {
if (is_Finish) {
if (i == 10) {
System.out.println("true");
}else{
System.out.println("false");
}
}
});
thread.start();
thread1.start();
}
}
上面代码,怎么看就该输出true,但是按照原来的思路来看会有过一个问题!
即当is_Finish请求的ACK比i的ACK要返回的快,该程序是会输出false的。is_Finish已经通过MESI协议了,i还没有通过,此时i还是0;
这就又有了一个新的问题,指令重排序。
为了解决重排序,设置了内存屏障,CPU在处理有内存屏障的数据时就不考虑CPU的利用率问题了,就会阻塞在这里,不去接下去运行其他指令,一定要等待ACK响应才会打开。
Java语言时一个跨平台的语言,不同的平台的硬件系统不一样,就导致了设置内存屏障方式的不同,JAVA为了解决这个问题,将对内存以及高速缓存的读写操作进行了抽象,这个抽象的读写过程就是JMM。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0tpneiS9-1619163985688)(C:\Users\15292\AppData\Roaming\Typora\typora-user-images\image-20210423152833258.png)]
这是Java层面的读写操作,我们通过这个读写操作去设置内存屏障,再去调用不同硬件系统的内存屏障的API,就达到了写一个JAVA程序,适应不同的硬件系统。
volatile、synchronized、final以及happens-before规则
happens-before就是表示一个代码的顺序关系
1 happens-before 2,代表1一定比2先执行。
程序顺序规则
监视器锁的规则
volatile变量规则
传递性
start()规则
线程终止原则
线程中断规则
join()规则
对象终结规则
在Java中,同步机制有很多种,其中volatile变量被称作为”轻量级“的同步机制,之所以被称之为”轻量级“同步机制,是因为在和sychronized关键词进行对比之下,volatile变量所需要的代码和运行时开销更少。当然,因为是”轻量级“的同步机制,volatile变量所具备的功能也仅仅是sy...
synchronized 锁重入 关键字 synchronized 拥有锁重入的功能,也就是在使用synchronized时,当一个线程得到一个对象锁后,再次请求此对象时可以再次得到该对象的锁。实例如下: 出现异常,锁自动释放 对于web应用程序,异常释放锁的情况,如果不及时处理,很可能对你的应用程序业务逻辑产生严重的错误。 比如你现在执行一个队列任务,很多对象都去在等待第一个对象正确执行完毕再去...
在多线程编程中,Synchronized 和 volatile 都扮演者重要的角色,前面的文章我们已经了解了java内置锁Synchronized ,它保证了并发过程中的可见性与原子性,避免了共享数据的错误。 而 Volatile可以看做是轻量级的 Synchronized,它只保证了共享变量的可见性。在线程 A 修改了被 volatile 修饰的共享变量之后,线程 B 能够读取到正确的值。在 关...
volatile关键字的特点 1)保证变量的可见性 也就是保证CPU在执行程序过程中,被volatile修饰的变量不再从高速缓存中找,而是直接从主存中寻找,从而保证每次都是变量最新的数据 2)保证有序性 JVM为了高效率的执行java程序,在不影响结果一致的情况下会对编译后的执行进行“重排序”。加入volatile关键字之后会保证该变量从创建到修改到使用的过程,JVM不会对...
java内存模型 所有的变量都存储在主内存(Main Memory)中。每个线程还有自己的工作内存(Working Memory),线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成。 可见性:可见性指多个线程操...
转载自:https://blog.csdn.net/paincupid/article/details/47346423 1.volatile volatile主要是用来在多线程中同步变量。 在一般情况下,为了提升性能,每个线程在运行时都会将主内存中的变量保存一份在自己的内存中作为变量副本,但是这样就很容易出现多个线程中保存的副本变量不一致,或与...
volatile关键字 关键字volatile的主要作用是修饰变量,每次使用强制从内存中取最新值。 在Java语言编写的程序中,有时为了提高程序的运行效率,编译器会自动对其进行优化,把经常被访问的变量缓存起来,程序在读取这个变量时有可能会直接从缓存(例如寄存器)中来读取这个值,而不会从内存中读取。这样做的一个好处是提高了程序的运行效率,但当遇到多线程编程时,变量的值可能因为别的线程而改变了,而缓存...
开始全文之前,先铺垫一下jvm基础知识以及线程栈: JVM栈是线程私有的,每个线程创建的同时都会创建JVM栈,JVM栈中存放的为当前线程中局部基本类型的变量(java中定义的八种基本类型:boolean、char、byte、short、int、long、float、double)、部分的返回结果以及Stack Frame,非基本类型的对象在JVM栈上仅存放一个指向堆上的地址。 &nb...
volatile的两个作用 阻止指令重排序 内存可见性(即每次变量的读取都从主存中读取,不从cpu高速缓存读取) volatile变量自增与线程安全性的测试 结论:volatile关键字无法保证volatile变量操作(非单独读写操作)的原子性,即volatile变量的操作不是线程安全的 以count++为例,这里可以分成3步原子性操作 1.从主存读取count值 2.执行+1操作(A线程执行此操...
使用systemd-coredump调试应用程序崩溃 systemd-coredump收集并显示内核核心转储,以分析应用程序崩溃。当某个进程崩溃(或所有属于某个应用程序的进程)时,其默认设置是将核心转储记录到日志中(systemd如果可能的话包括回溯),并将核心转储存储在中的文件中 /var/lib/systemd/coredump。您还可以选择使用其他工具(例如gdb或crash) 检查转储文件...