JMM内存模型理解
一、JMM即Java Memory Model,也就是java内存模型。
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,
但线程对变量的操作(读取、赋值等)都必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,
不能直接操作主内存中的变量,各线程中的工作内存中存储着主内存中的
变量拷贝副本
,因此不同线程间不能访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成
1.1、JMM特性:
1、可见性
2、原子性
3、有序性
1.2、看下面代码:
class MyVolatile{
int a =0;
public void add(){
this.a = 20;
}
}
public class JMMUtils {
public static void main(String[] args) {
MyVolatile mv = new MyVolatile();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
}catch (Exception e){
e.printStackTrace();
}
mv.add();
System.out.println("当前子线程:"+Thread.currentThread().getName()+"当前值:"+mv.a);
},"son").start();
while (mv.a == 0){
}
System.out.println("当前主线程:"+Thread.currentThread().getName()+"当前值:"+mv.a);
}
}
这段代码执行的结果是:
当前子线程:son当前值:20
1.3、这里可以看到,线程son改变了变量a的值,但主线程并没有看到,这是为什么呢,这是不是与上面的可见性矛盾?
计算机在运行程序时,每条指令都是在CPU中执行的,在执行过程中势必会涉及到数据的读写。我们知道程序运行的数据是存储在主存中,这时就会有一个问题,
读写主存中的数据没有CPU中执行指令的速度快
,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了CPU高速缓存。CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。
有了CPU高速缓存虽然解决了效率问题,但是它会带来一个新的问题:数据一致性。在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中,
在进行运算时CPU不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后才会将数据刷新到主存中
。
这就是上面main线程获取到的值依然是0的原因!
1.4、如何解决数据一致性问题呢?
1、通过在总线加LOCK锁的方式;
2、
通过缓存一致性协议
3、将 Runnable 对象作为一个类的内部类,共享数据作为这个类的成员变量,每个线程对共享数据的操作方法也封装在外部类,以便实现对数据的各个操作的同步和互斥,作为内部类的各个
Runnable 对象调用外部类的这些方法
MESI协议是指:
M(modified)修改、E(exclusive)独享、互斥、S(shared)共享、I(invalid)无效的
。
多个cpu从主内存读取同一个数据到各自的高速缓存中,其中当某个cpu修改了缓存里的数据,该数据马上同步回主内存,其他cpu通过总线嗅探机制,可以感知到数据的变化从而将自己缓存的数据失效。
MESI协议只对汇编指令中执行加锁操作的变量有效,表现到java中为使用voliate关键字定义变量或使用加锁操作
回到上面的java代码:
我们把 int a =0; 改为 volatile int a =0;
再执行,输出:
当前子线程:son当前值:20
当前主线程:main当前值:20
这次主线程感知到了变量的变化!
二、关键字volatile
2.1、volatile实现原理
有volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令,该指令在多核处理器下会引发两件事情。
1、将当前处理器缓存行数据刷写到系统主内存。
2、这个刷写回主内存的操作会使其他CPU缓存的该共享变量内存地址的数据无效。
这样就保证了多个处理器的缓存是一致的,对应的处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器缓存行设置无效状态,当处理器对这个数据进行修改操作的时候会重新从主内存中把数据读取到缓存里
2.2、volatile的特性:
1、可见性
定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
2、不保证原子性
原子性 定义: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行
3、禁止指令重排
对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为Memory
Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证
2.3、volatile使用场景:
1、中断服务程序中修改的供其它程序检测的变量需要加volatile;
2、多任务环境下各任务间共享的标志应该加volatile;
3、存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义
三、linux安装jdk的两种方法
3.1 压缩包安装
1.上传jdk-8u221-linux-x64.tar.gz到服务器指定目录,例如我放在/usr/local/jdk下
2.解压 tar -zxvf jdk-8u221-linux-x64.tar.gz
3.配置环境:vim /etc/profile
export JAVA_HOME=/usr/local/jdk/jdk1.8.0_221
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export JRE_HOME=${JAVA_HOME}/jre
4.重新加载配置:source /etc/profile
此时即安装完毕,验证:java -version 或者 jps
3.2 yum源安装
1.查询包:
yum list java-1.8*
2.查询安装路径:
rpm -ql java-1.8.0-openjdk-devel.x86_64
3.yum安装:
yum -y install java-1.8.0-openjdk-devel.x86_64
4.配置环境变量:
vim /etc/profile
export JAVA_HOME=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.362.b08-1.el7_9.x86_64
export PATH=$JAVA_HOME/bin:$PATH
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
export JRE_HOME=${JAVA_HOME}/jre
5.重新加载:source /etc/profile