JMM与volatile

1.JMM是什么?

jmm是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在本地内存数据不一致,编译器会对代码指令重排序,处理器会对代码乱序执行带来的问题

JMM并不是实际存在的内存模型,它是一个规范,主要围绕并发过程中如何处理可见性,原子性,有序性这三方面而建立的模型

JMM规定了所有变量存储在主内存里面,每个线程有自己的工作内存,线程的工作内存保存了该线程使用到的变量的主内存的拷贝

线程对变量的造作都是在工作内存里面完成的,不能直接读写主线程

不同线程之间不能相互访问对方工作线程里面的变量,线程之间的值传递都要通过主内存来完成

主内存:对应计算机里面的内存,从某种程度上来说的话包含堆和方法区

工作内存:是JMM的一个抽象的概念,实际并不存在,它包含了缓存,写缓冲区,寄存器,程序计数器,虚拟机栈,本地方法栈以及其他硬件以及编译器优化之后的一个数据存放位置

2.JMM解决什么问题?

主要是为了解决原子性,可见性和有序性三个问题

原子性:类似于事务的概念,所有操作要么都执行,要么都不执行,在java里面,大部分基础数据类型都是原子操作,long和double类型在32位操作系统里面不是原子操作,因为long和double是64位数据类型,这也意味着在32位操作系统下long和double多线程环境下是不安全的

在java里面可以通过synchronized关键字或者Lock对代码块加锁保证原子性

可见性:一个线程对一个变量修改后其他线程可以立即看到(感知到)对应的变化

单线程环境下不存在可见性问题,因为单线程环境都是顺序操作的,多线程环境下会出现可见性问题,比如一个线程A把一个变量load到内存里面并对他进行修改,还未写回主存,这是另外一个线程B又来读取这个变量,但此时A线程工作内存里面的共享变量对B线程并不可见,这就是可见性问题

在java里面可以通过synchronized关键字,lock,volatile,final实现可见性

有序性:在jvm里面会对没有数据依赖的代码进行重排序,比如1,2,3可能重排序成3,2,1,这种情况在单线程环境下不会有什么问题,但是多线程环境下就可能会有问题了

在java里面可以用volatile防止指令重排来保证一定的有序性,也可以使用synchronized和Lock保证同一时刻只有一个线程在操作

3.JMM的八大原子操作

lock:锁定,作用于主内存的变量,把一个变量标记为一个线程独占状态
unlock:解锁,作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read:读取,作用于主内存变量,把一个变量值从主内存读出来,以便于随后的load工作使用
load:载入,将主内存读出来的变量值放到工作内存的变量中
use: 作用于工作内存变量,把工作内存的变量值传递给执行引擎
assign:赋值,作用于工作内存变量,把一个从执行引擎接收到的值赋值给工作内存的变量
store:作用于工作内存变量,把工作内存变量值传送到主内存中,以便于随后的write操作使用
write:作用于工作内存变量,把store操作从工作内存中的一个变量值传送到主内存变量中

如果要把一个变量值从主内存复制到工作内存,那么就要按顺序执行read和load
如果要把工作内存里面的变量值赋值到主内存中,那么就要按顺序执行store和write操作
但是java内存模型只是规定上述操作要按照顺序执行,但是不保证是连续执行的
同步规则分析:
不允许一个线程无原因地(没有发生任何 assign 操作)把数据从工作内存同步回主内存中;
一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或者 assign)的变量。即就是对一个变量实施 use 和 store 操作之前,必须先自行 assign 和 load 操作;
一个变量在同一时刻只允许一条线程对其进行 lock 操作,但 lock 操作可不被同一线程重复执行多次,多次执行 lock 后,只有执行相同次数 unlock 操作,变量才会被解锁。lock 和 unlock 必须成对出现;
如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用变量之前需要重新执行 load 或 assign 操作初始化变量的值;
如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作;也不允许去 unlock 一个被其他线程锁定的变量;
对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行store 和 write 操作)。

4.happens-before

原则:

如果一个操作happends-before另外一个操作,那么第一个操作执行结果对第二个操作可见,且第一个操作执行顺序排在第二个操作之前

两个操作之间存在happends-before关系,并不意味着要一定按照happends-before原则制定的顺序来执行,如果重排序之后的执行结果与按照happens-before关系来执行的结果一致,那么这种重排序并不非法。

程序次序规则:一段代码在单线程中执行的结果是有序的。注意是执行结果,因为虚拟机、处理器会对指令进行重排序(重排序后面会详细介绍)。虽然重排序了,但是并不会影响程序的执行结果,所以程序最终执行的结果与顺序执行的结果是一致的。故而这个规则只对单线程有效,在多线程环境下无法保证正确性。

锁定规则:这个规则比较好理解,无论是在单线程环境还是多线程环境,一个锁处于被锁定状态,那么必须先执行unlock操作后面才能进行lock操作。

volatile变量规则:这是一条比较重要的规则,它标志着volatile保证了线程可见性。通俗点讲就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作一定是happens-before读操作的。

传递规则:提现了happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C

线程启动规则:假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行后确保对线程B可见。

线程终结规则:假定线程A在执行的过程中,通过制定ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。

线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生;

对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;

上面是原生的Happens-before规则,但是我们可以对他们推导出其他满足happens-before的规则:

1.将一个元素放入一个线程安全的容器里happens-before从容器里取出这个元素

2.在CountDownLatch上的倒数操作Happens-before countDownLatch的await操作

3.释放Semaphore许可操作happends-before获得许可操作

4.Future表示的任务的所有的操作happends-before Future的get操作

5.向一个Executor提交一个Runnable或Callable的操作Happens-before任务开始执行操作

如果两个操作或者多个操作不能满足以上任意一个规则,那就代表没有顺序保障,jvm就可以对这操作进行重排序,也就是多线程不安全的

as-if-serial:不管怎么重排序,单线程下的执行结果都不能改变,编译器,runtime和处理器都必须遵守as-if-serial语义

为了遵循as-if-serial语义,数据之间存在依赖关系的不会进行重排序,因为这种重排序可能会影响最终执行结果,但是,如果操作之间不存在数据依赖关系,那这些操作就可能会被重排序了

5.volatile如何保证可见性

volatile保证了数据可见性,一定程度的有序性,无法保证原子性

可见性保证:在汇编层面增加lock使用缓存一致性协议(mesi)解决并发可见问题

首先cpu会根据共享变量是否带有volatile字段,决定是否使用MESI协议保证缓存一致性,如果有volatile关键字,在汇编层面会加上LOCK前缀,当一个变量被修改后会马上经过store,write等原子操作修改主内存值(如果不加lock前缀不会马上同步),为了触发cpu的嗅探机制,及时将其他线程的变量副本置为失效

cpu总线嗅探机制监听到这个变量被修改,就会把其他线程的变量副本由共享S置为无效I,当其他线程在使用变量副本时,发现其已经无效,就回去主内存中拿一个最新的值。

变量被修改后写入主内存时需要加锁,写完之后解锁,这个锁只有在修改时才会加,锁力度非常小,因为在store是可能已经过了总线,但是还没write进主存,总线却触发了嗅探机制,其他线程变量已经失效,当其他线程去主内存读最新数据时,新数据还未write进来,产生脏数据

有序性保证:happends-before里面有一条规则是:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

Java 编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。

重排规则表:

" NO " 表示禁止重排序。

为了实现 volatile 内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM 采取了保守的策略。

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

volatile 写是在前面和后面分别插入内存屏障,而 volatile 读操作是在后面插入两个内存屏障。

 

内存屏障说明
StoreStore 屏障禁止上面的普通写和下面的 volatile 写重排序。
StoreLoad 屏障防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。
LoadLoad 屏障禁止下面所有的普通读操作和上面的 volatile 读重排序。
LoadStore 屏障禁止下面所有的普通写操作和上面的 volatile 读重排序。

 

原子性保证 :volatile可以保证单次读写的原子性,如long和double,但是并不能保证i++这种操作的原子性,因为本质上i++是复合操作:先读取i的值,对i加1,将i的值写回内存,volatile无法保证这三个操作的原子性(如果需要的话我们可以通过AtomicInteger或者Synchronized来保证+1操作的原子性)

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值