jmm内存可见性与CAS

前言:在慕课网上学习剑指Java面试-Offer直通车时所做的笔记,供本人复习之用.

目录

第一章 Java内存模型

第二章 JMM中的主内存和工作内存 

2.1 主内存与工作内存介绍

2.2 JMM与JVM的区别

2.3 可见性问题

2.4 指令重排序需要满足的条件

2.5 happens-before原则

2.5.1 例1

2.5.2 volatile 

2.5.3 例2

2.5.4 例3

2.5.5 例4

第三章 CAS(Compare and Swap)

3.1 AtomicInteger


 

第一章 Java内存模型

 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存,有些地方成为栈空间,用于存储线程私有的数据,而java内存模型中规定,所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问,线程对变量的操作如读取,赋值等必须在工作内存中进行,首先将变量从主内存拷贝到自己的工作内存中,然后对变量操作,操作后再将变量写回主内存.不能直接操作主内存中的变量,工作内存中存储着主内存中变量的副本拷贝,工作内存是每个变量的私有区域,因此不同线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成.

第二章 JMM中的主内存和工作内存 

2.1 主内存与工作内存介绍

JMM中的主内存: 

JMM中的工作内存:

注意第一条,即使两个线程执行的是同一个方法,它们也会在自身的工作内存中创建当前线程的工作变量.

2.2 JMM与JVM的区别

JMM与JVM内存区域划分是不同的概念层次.

JMM描述的是一组规则,通过这组规则,控制程序中各个变量在共享数据区域和私有数据区域的访问方式,jmm是围绕原子性,有序性,可见性展开的.

相似点:存在共享区域和私有区域.

JMM中主内存是共享数据区域,应该包括堆和方法区,而工作内存数据线程私有数据区域某个程度上讲,应该包括程序计数器,虚拟机栈,以及本地方法栈.

对于一个实例中成员方法而言,如果方法中包含的本地变量是基本数据类型的(那8种),这些本地变量将直接存储在工作内存的栈帧结构中.

倘若本地变量是引用类型的,该变量的引用会存储在工作内存的栈帧中,而对象实例则存储在主内存.即我们前面说的共享区域堆当中.

对于b实例对象的成员变量,static变量,类信息均会被存储在主内存的堆区.

主内存的实例对象可以被多线程共享,倘若两个线程调用了同一个对象的同一个方法,两个线程会将要操作的数据拷贝一份到自己的工作内存中,对数据操作完成后刷新到主内存中.

2.3 可见性问题

把数据从内存加载到缓存寄存器,运算结束,写回主内存.

但是当线程共享变量的时候,情况就变得非常复杂了,如果处理器对某个变量进行了修改,可能只是体现在该内核的缓存里,而运行在其它内核上的线程可能加载的是旧状态,这很可能导致一致性的问题,从理论上来说,多线程共享引入了复杂的数据依赖性问题,不管处理器,编译器怎么做重排序都必须尊重数据依赖型的要求,否则就打破了数据的正确性.这就是jmm所要解决的问题.

2.4 指令重排序需要满足的条件

在执行程序的时候,为了提高性能,处理器和编译器常常会对指令进行重排序,需要满足以下条件:

以上两点可以归结为一点:

jmm内部的实现通常是依赖于所谓的内存屏障,通过禁止某些重排序的方式提供内存可见性保证,也就是实现了各种happens-before的规则,更多的复杂度在于,需要尽量确保各种编译器,各种体系结构的处理器,能够提供一致的行为.

在jmm中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须满足happens-before的关系.

2.5 happens-before原则

happens-before原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依据,依靠此原则我们变能解决在并发环境下两个操作存在冲突的问题.

我们分析以下代码,假设线程A happens-before线程B,即线程A先于线程B发生,可以确定线程B操作后j是1是确定的,如果它们不存在happens-before原则,那么j=1就不一定能够成立.

happens-before的八大原则:

2.5.1 例1

我们约定线程A执行write操作,线程B执行read操作,且线程A优先于线程B去执行,那么线程B获得的结果是什么呢?

可以看到5,6,7,8这四个规则是可以被忽略的,因为与这段代码毫无关系.

两个线程,规则1不适用,没有锁,规则2不适用,规则3肯定也不适用,没使用volatile,规则4也不适合.

所以无法通过happens-before原则推导出A happens-before B,不知道B什么时候执行.

所以这段代码不是线程安全的.

我们只需满足2,3规则中的一个即可保证线程安全,即加同步锁或者volatile.

2.5.2 volatile 

volatile:JVM提供的轻量级同步机制

volatile的作用:

volatile的可见性:我们必须意识到volatile修饰的变量总是对所有线程立即可见的,对volatile的所有写操作总是能立即反映到其它线程中,但是对于volatile运算操作在多线程环境中并不保证安全性.

volatile变量为何立即可见?

volatile如何禁止重排优化?

首先要了解内存屏障:

 volatile正是通过内存屏障实现其在内存中的语义即可见性和禁止重排优化.

volatile和synchronized的区别:

2.5.3 例2

value变量的任何改变都会反映到线程中,但是若有多条线程同时访问increase方法,就会出现线程安全问题,毕竟value++操作并不具备原子性.

value++操作是先读取值,然后再写回一个新值,相当于原来的值加1分两步来完成.如果第二个线程在第一个线程读取旧值写回新值之间,读取value的值,那么第二个线程就会与第一个线程一起看到同一个值.并执行相同的加1操作,引发了线程安全问题.所以对于increase必须用synchronized修饰,以便保证线程安全,需要补充并且注意的是,synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这就使被synchronized保护的代码块无法被其它线程访问,也就无法并发执行.

修改线程安全:

synchronized会创建一个内存屏障指令,其保证了所有CPU结果都会直接刷到主存中,从而保证了操作的内存可见性.也保证了顺序执行.

2.5.4 例3

由于对boolen的修改属于原子操作,因此可以使volatile修饰该变量,使其修改对其它线程立即可见,从而达到线程安全的目的.

2.5.5 例4

面试时经常要写的所谓实现线程安全的单例写法,通过引入synchronized代码块试图解决多线程请求单例时重复创建单例的隐患.下面的代码在多线程环境下依然会有隐患.

原因:

new singleton()创建时会有三步

并可以有如下的重排序优化.这样就可能导致getInstance返回null,一个线程走到了第二步,memory还是空,另一个线程判断instance不是null,直接返回memory,造成错误.

解决方法如下,使用volatile使instance禁止指令进行重排序即可,即2,和3不能颠倒过来.

 

第三章 CAS(Compare and Swap)

像synchronized这种独占锁属于悲观锁,悲观锁始终假定,因此会屏蔽一切可能违反数据完整性的操作,除此之外,还有乐观锁,它假定不会发生并发冲突,因此只在提交操作时检查是否违反数据完整性,如果提交失败则会进行重试,而乐观锁最常见的就是CAS.

CAS是一种高效实现线程安全性的方法.

CAS思想:

包含三个操作数-内存位置(V),预期原值(A)和新值(B)

将内存位置的值与预期原值进行比较,如果相匹配则处理器会自动将内存位置的值更新为新值,否则处理器不做任何操作,这里内存位置的值V即主内存的值.

举个例子,当一个线程需要修改共享变量的值,完成这个操作先取出共享变量的值赋给A,基于A的基础进行计算得到新值B,执行完毕需要更新共享变量的值的时候,我们调用CAS方法去更新共享变量的值.

看一下之前的例子:

查看其字节码:

可以看到value++被拆分成了如下的指令,首先需要getfield拿到原始的value,也就是从我们的主内存中将value加载进当前线程的工作内存中,执行iadd进行+1的操作,之后再执行putfield把累加后的值写回我们的主内存当中.

通过volatile修饰的变量,可以保证线程之间的可见性,同时也不允许JVM对它们进行重排序.但是并不能保证这三个指令的原子执行,在多线程并发下,无法做到线程安全.

该如何解决呢?

在add前加入synchronized操作即可解决.

但是能否尽量提升性能呢?

3.1 AtomicInteger

可以使用AtomicInteger来满足需求,其位于concurrent.atomic包中,

从AtomicInteger的内部属性可以看出,它依赖于unsafe提供的一些底层能力,进行底层操作,以volatile的value字段记录数值以保证可见性.

其中的getAndIncrement方法可以解决上面value++的不安全性.此方法会利用value字段的地址偏移直接完成操作.

点进getAndIncrement,因为需要返回数值多以需要添加失败重试的逻辑.

 而向返回布尔类型的,因为其返回值表现得就是成功与否,所以不需要进行重试.

Unsafe里的这些方法,如compareAndSetInt 则实现了CAS.

 

CAS多数情况下对开发者来说是透明的,我们更多的是使用并发包间接享受到lock-free机制在扩展性上的好处.

CAS缺点:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章