JMM即为 Java Memory Model ,他定义了主存(多个线程所共享的空间、例:成员变量)、工作内存(线程的私有空间,例:局部变量)的抽象概念,对应着底层的CPU寄存器、缓存、硬件内存、CPU指令优化等;
概要:我们通过操作java这些抽象概念,间接的操作复杂底层(化繁为简)
JMM体现在以下的几个方面 :
看一个现象:
public class VisibleTest {
static boolean isrun = true ;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
while(isrun){
}
}, "T1");
t1.start();
Thread.sleep(1000);
System.out.println("T1线程停止");
isrun = false ;
//线程t1并不会如预想的一样停下来!
}
}
测试结果:
为什么会这样?分析一下:
1、初始状态,T1线程从主存当中读取了run的值到工作内存;
2、因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率(JIT : Just In Time Compiler,一般翻译为即时编译器,)
1秒之后,main线程修改了run的值,并同步至主存,而+是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值
方法一:为变量添加修饰:volatile(易变化关键字)
volatile static boolean isrun = true ;
这样做的目的是:加上volatile 的变量,每次循环都是只能在主存当中获取,不会从高速缓存区中获取!
测试结果:T1线程停止
方法二:使用synchronized
在Java内存模型中,synchronized规定,线程在加锁时, 先清空工作内存→在主内存中拷贝最新变量的副本到工作内存 →执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。(线程获得对象锁后,会清空工作区内存,重新在主存中获取!)
重点区分:volatile和synchronized ;
JVM会在不影响正确的条件下,调整语句的执行顺序!这种特性称作【指令重排】
//如下i和j的++操作调换顺序不影响结果!
public class ReSortTest {
static int i = 0 ;
static int j = 0 ;
public static void main(String[] args) {
i++; //修改为j++
j++; //修改为i++
}
}
思考:正常执行是正确的,而且多线程条件下指令重排可能是会出现问题的,为什么要进行指令重排的优化呢?
事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的CPU指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令—指令译码—执行指令—内存访问—数据写回这5个阶段
重排之前:指令串行执行!
现代CPU支持多级指令流水线,例如支持同时执行取指令~指令译码–执行指令–内存访问–数据写回的处理器,就可以称之为五级指令流水线。这时CPU可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC =1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。
重拍之后:指令并行执行 !
总结:指令级别的优化,我们线程的不同指令的不同阶段可同时进行!【指令级别的并发】
重排序的目的:为的是一个指令执行某一个阶段的时候,通过重排序,让其他执行执行其他的阶段!达到最大的指令并发!
当然前提是:重排互不影响结果 !
public class ReSortTest {
static int i = 0 ;
static int j = 0 ;
public static void main(String[] args) {
i++; //2条指令可重排序!
j++;
i= j - 10 ; //不可重排序,会影响结果
j++ ;
}
}
禁止指令重排序
可以使用volatile实现,因为volatile可以使得被修饰的变量之前的操作是不会被重排序的
以上可以了解到Volatile可以保证共享变量的有序性、可见性 , 我们接下来了解一下原理 ;
volatile的底层实现原理是内存屏障,Memory Barrier (Memory Fence)
public class ReSortTest {
static int i = 0 ;
volatile static int j = 0 ;
public static void main(String[] args) {
i++;
j++; //由于j是volatile修饰的,此处对j进行写操作,所以插入写屏障 !
//所以j++ 以及之前的代码全部会被同步到主存当中
}
}
public class ReSortTest {
volatile static int j = 0 ;
public static void main(String[] args) {
if(j > 1){
//读取j ,在此之前的代码插入读屏障!(保证读取到的是主存中最新的数据!)
}
}
}
public class ReSortTest {
static int i = 0 ;
volatile static int j = 0 ;
public static void main(String[] args) {
i++;
j++; //由于j是volatile修饰的,此处对j进行写操作,所以插入写屏障 !
//所以j++ 以及之前的代码全部会被同步到主存当中
//写屏障 , 之前的代码不会发生指令重排序!
}
}
public class ReSortTest {
volatile static int j = 0 ;
public static void main(String[] args) {
//读屏障:之后的代码不会被指令重排序
if(j > 1){
//读取j ,在此之前的代码插入读屏障!(保证读取到的是主存中最新的数据!)
}
}
}
总结:
虽然能解决可见性和有序性,但是仍然不能解决指令交错问题(原子性) ;
DCL : Double Check Locking 双检锁
看如下代码:
// 最开始的单例模式是这样的
public final class Singleton {
private Singleton() {
}
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
// 首次访问会同步,而之后的使用不用进入synchronized
synchronized(Singleton.class) {
if (INSTANCE == null) {
// t1
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
// 但是上面的代码块的效率是有问题的,因为即使已经产生了单实例之后,之后调用了getInstance()方法之后还是会加锁,这会严重影响性能!因此就有了模式如下double-checked lockin:
public final class Singleton {
private Singleton() {
}
private static Singleton INSTANCE = null;
public static Singleton getInstance() {
if(INSTANCE == null) {
// t2 #1
// 首次访问会同步,而之后的使用没有 synchronized
synchronized(Singleton.class) {
#2
if (INSTANCE == null) {
// t1 #3
INSTANCE = new Singleton(); #4
}
}
}
return INSTANCE; #5
}
}
//但是上面的if(INSTANCE == null)判断代码没有在同步代码块synchronized中,不能享有synchronized保证的原子性,可见性。所以
以上的实现特点是:
多线程情况下,上述代码仍然存在指令重排的问题
当我们的线程t1,执行到 if(INSTANCE == null) #3,发现此时的实例为null,就去获取锁创建对象,我们看一下new对象的的字节码指令
// 通过这个复制的引用调用它的构造方法
21: invokespecial #4 // Method "":()V
// 最开始的这个引用用来进行赋值操作
24: putstatic #2 // Field INSTANCE:Lcn/itcast/n5/Singleton;
此时我们的发生指令重排,先执行赋值操作,先将空的实例对象返回(此时Instance实例已经有值了),然后执行构造初始化对象!
就在我们的的初始化执行一半,线程t2过来了,发现instance不为null,执行 return INSTANCE;我们此时返回还是未被初始化的对象,所
以问题就此发生!!
解决DCL
加volatile就行了。
public final class Singleton {
private Singleton() {
}
private static volatile Singleton INSTANCE = null;
public static Singleton getInstance() {
// 实例没创建,才会进入内部的 synchronized代码块
if (INSTANCE == null) {
synchronized (Singleton.class) {
// t2
// 也许有其它线程已经创建实例,所以再判断一次
if (INSTANCE == null) {
// t1
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
对volatile修饰的变量进行些操作的时候,在写操作后加上内存屏障,使得写屏障之前的代码不会发生指令重排!
七大规则(保证共享变量可见性的七种方法)!
CAS + Volatile 无锁实现并发,保证线程安全(乐观锁)
CAS (Compare And Set) : 比较并设置
//测试代码!
public class CasTest02 {
AtomicInteger balance2 = new AtomicInteger(100);
public void withdraw(Integer amount){
while(true){
int pre = balance2.get() ;
int next = pre - amount ;
if(balance2.compareAndSet(pre,next)){
System.out.println(balance2.get()); //90
break ; //比较并设置设置值
}
}
}
}
class TestCas{
public static void main(String[] args) {
CasTest02 test02 = new CasTest02();
test02.withdraw(10);
}
//多个线程访问如下方法
其中ComapreAndSet,简称就是CAS(也有Compare And Swap的说法) ,它必须是原子操作!
当CAS方法执行时,prev 会与主存的实时balance比较一次,如果发现不一致(其他线程修改了),那么就返回false ;
//源码
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update); //执行cas时expect会与自身value比较
}
获取共享变量时,为了保证该变量的可见性,需要使用volatile修饰。
它可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。即一个线程对volatile变量的修改,对另一个线程可见。
注意
volatile仅仅保证了共享变量的可见性,让其它线程能够看到最新值,但不能解决指令交错问题(不能保证原子性)
//在我们原子整数当中,value都是被volatile修饰过的!
private volatile int value;
CAS必须借助volatile才能读取到共享变量的最新值来实现【比较并交换】的效果
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。
而且最好用于线程数少于核心数的情况,线程数多的话CAS所在线程分不到时间片依然会进行上下文切换!
总结:因为CAS无锁保证线程安全的话,线程不会说会受到其他线程的影响陷入BLOCK阻塞状态,而是多个线程都会操作共享对象,但是cas会一直比较保证线程安全,线程是不会停止的,sync有锁方式则会出现一个线程获得锁,其他线程只能陷入BLOCK状态等待!
JUC的子包 java.util.concurrent.atomic 提供了
AtomicInteger为例:
AtomicInteger i = new AtomicInteger(0);
//下边方法属于原子方法,线程安全的!
System.out.println(i.getAndIncrement()); // 结果为 0 等价 i ++ (线程不安全的!)
System.out.println(i.incrementAndGet()); // 结果为 2 等价 ++ i
System.out.println(i.getAndAdd(5));// 结果2
System.out.println(i.addAndGet(5));// 结果12
读取到的 要更改为
System.out.println(i.updateAndGet(x -> x * 10)); //输出 50
//本质都是compare and set ;
除了保护我们的基本类型,还可以保护BigDecimal这种引用类型 ;
//测试代码!
private AtomicReference<BigDecimal> baclace ; //外加一层AtomicReference
public void withdraw(Integer amount){
while(true){
BigDecimal pre = balance.get() ;
BigDecimal next = pre.subtract(amount) ; //引用数据类型减法
if (balance.ComapreAndSet(pre,next)) break ; //比较并设置设置值
}
}
BigDecimal decimal = new BigDecimal("1000"); //初始化时最好传递的时字符串!
我们都知道我们cas保证的时最新的值和pre是否相等来判断是否被修改,但是存在这么一种情况:值被修改但是,修改后还是跟pre一致,这种情况,cas则无法判断是否被修改过 ;(虽然对业务无影响,但是仍是个隐患!)
因此,为了解决ABA这种问题引入
AtomicStampedReference<String> str = new AtomicStampedReference<>("a",0); // 0相当于版本号,只要修改过就会 + 1
//除了比较值是否相等还会比较版本号,版本号会记录改过的次数
想对上面AtomicMarkableReference只关心是否被修改过,并不关心修改的次数
AtomicMarkableReference<String> s = new AtomicMarkableReference<>("123",false);
保护数组里面的元素、有点复杂没看懂涉及JDK8新特性
保护某个对象里的属性、保证多个线程访问对象中属性的安全性!
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
public class AtomicField {
public static void main(String[] args) {
Student student = new Student(); //多个线程修改其中的name属性
//为Student的name属性设置更新器
AtomicReferenceFieldUpdater updater = //类 属性类型 属性名
AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");
updater.compareAndSet(student,null,"张三");
System.out.println(student); //Student{name='张三'}
}
}
class Student{
volatile String name ; //必须volatile修饰、不然抛出异常!
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
通过打断点Debug模拟其他线程提前操作,导致cas匹配失败!
在进行累加的时候,JDK提供如下的2个类的性能是优越于AtomicInteger、AtomicLong这些的,提高4、5倍!
性能提升的原因很简单,就是在有竞争时,我们的AtomicLong向一个累加单元多次尝试,会降低效率,然而LongAdder设置多个累加单元,Therad-0累加Cell[0],而Thread-1 累加Cell[1]…最后将结果汇总。这样它们在累加时操作的不同的Cell变量,因此减少了CAS重试失败从而提高性能。
其中的Cell为累加单元
//防止缓存行伪共享
@sun.misc.contended
public static final class cell {
volatile long value;
cell( long x) {
value = x; }
//最重要的方法,用来 cas.方式进行累加,prev表示旧值,next表示新值
final boolean cas( long prev,long next) {
return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next);
}
//省略不重要代码
解释这个需要从CPU的缓存说起
因为Cell 是数组形式,在内存中是连续存储的,一个Cell为24字节(16字节的对象头和8字节的value),因此缓存行可以存下2个的Cell对象。这样问题来了:
无论谁修改成功,都会导致对方Core的缓存行失效,比如Core-0中ce11[0]=6000,Cell[1]=800。要累加cell[e]=6001,cell[1]=800e,这时会让Core-1的缓存行失效
@sun.misc.Contended用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,从而让CPU将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效
总结:
然而缓存行也存在问题:假设CPU的第一个核需要操作a变量,第二个核需要操作b变量,表面看a和b是没有任何关系的,但是a和b在同一个cache line中,这样假设核心一修改了变量a的值,那么它将会刷新所有和a相关的缓存的数据,b变量也就会受到牵连,最后导致核心二再去缓存读取b变量的时候出现cache miss,需要重新到主存加载新数据,这就是所谓的false share(伪共享) !
我们可以用Contended注解使得我们的累加单元分别保存在不同的缓存行!
//源码
public void add(long x) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
总结一个add的流程图
总结LongAccumulate流程图
我们获取最终的累加结果
//源码
public long sum() {
Cell[] as = cells; Cell a;
long sum = base;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}