Java内存模型

Java内存模型

JMM即java memory model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU指令优化等。

JMM体现在以下几个方面

· 原子性 - 保证指令不受到线程上下文切换的影响(之前的synchornized原理文章有介绍过)

· 可见性 - 保证指令不会受CPU缓存的影响

· 有序性 - 保证指令不会受CPU指令并行优化的影响

可见性

先看一个现象,main线程对run变量的修改对于t线程不可见,导致了t线程无法停止:

@Slf4j(topic = "c.TestDemo")
public class TestDemo {
    static boolean run = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (run){
            }
        }).start();
        Thread.sleep(1000);
        log.debug("修改run");
        run = false;
    }
}

1.初始状态,t线程刚开始从主内存读取了run的值到工作内存。

在这里插入图片描述

2.因为t线程要频繁从主内存中读取run的值,JIT编译器会将run的值缓存至自己工作内存中的高速缓存中,减少对主存中run的访问,提高效率

在这里插入图片描述

3.1秒之后,main线程修改了run的值,并同步至主存,而t是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

在这里插入图片描述

解决方法

volatile(易变关键字)

它可以用来修饰成员变量和静态成员变量,它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作volatile变量都是直接操作主存。

volatile static boolean run = true;
public static void main(String[] args) throws InterruptedException {
    new Thread(()->{
        while (run){
        }
    }).start();
    Thread.sleep(1000);
    log.debug("修改run");
    run = false;
}
加锁也可以保证可见性
static boolean run = true;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
    new Thread(()->{
        while (run){
            synchronized (lock){
                if (!run){
                    break;
                }
            }
        }
    },"t1").start();
    Thread.sleep(1000);
    log.debug("停止t");
    synchronized (lock){
        run = false;
    }
}

但是由于synchronized需要给对象创建monitor比较重量,所以建议使用volatile

可见性&原子性

前一个例子体现的实际就是可见性,它保证的是在多个线程之间,一个线程对volatile变量的修改对另一个线程可见,不能保证原子性,仅用在一个写线程,多个读线程的情况

在这里插入图片描述

比较一下之前我们将线程安全时举的例子:两个线程一个i++一个i–,只能保证看到最新值,不能解决指令交错

在这里插入图片描述

注意

synchronized语句块既可以保证代码块的原子性,也可以保证代码块的可见性。但缺点是synchronized是属于重量级操作,性能相对较低

如果前边示例死循环中加入System.out.println()不用volatile修饰符,线程t也能正确看到run变量的修改。

有序性

JVM会在不影响正确性的前提下,可以调整语句的执行顺序。

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;

可以看到,至于是先执行i还是先执行j,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是先给i赋值也可以是先给j赋值

这种特性称之为指令重排,多线程下指令重排会影响正确性。

指令重排的前提时,重排指令不能影响结果

//可以重排的例子
int a = 10;//指令1
int b = 20;//指令2
System.out.println(a+b);

//不能重排的例子
int a = 10;//指令1
int b = a - 5;//指令2

为什么会产生指令重排呢?

事实上,现代处理器会设计为一个时钟周期完成一条执行时间最长的CPU指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每个指令都可以分为:取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这5个阶段

在这里插入图片描述

术语参考:

· instruction fetch(IF)

· instruction decode(ID)

· execute(EX)

· memory access(MEM)

· register write back(WB)

现代CPU支持多级指令流水线,例如支持同时执行 IF - ID - EX - MEM - WB 的处理器,就可以称为五级指令流水线。这时CPU可以在一个时钟周期内,同时运行五条指令的不同阶段,IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但他变相地提高了指令地吞吐率

在这里插入图片描述

指令重排序在多线程下的影响

int num = 0;
boolean ready = false;

//线程1 执行此方法
public void actor1(I_Result r){
	if(ready){
		r.r1 = num + num;
	}else{
		r.r1 = 1;
	}
}

//线程2 执行此方法
public void actor2(I_Result r){
	num = 2;
	ready = true;
}

结果会出现四种情况

情况1: 线程1先执行,这时ready=false,所有进入else分支结果为1

情况2: 线程2限制性num=2,但没来得及执行ready = true,线程1执行,还是进入else分支,结果为1

情况3: 线程二执行到ready=true,线程1执行,结果为4

情况4:线程2执行ready=true,切换到线程1,进入if分支,相加为0,再切回线程2执行num=2

情况4这种情况就是指令重排的体现

这里只需在ready 变量前加上volatile关键字就行了(不在num属性上加是因为volatile关键字能确保之前的代码也不指令重排 )

happens-before

happens-before规定了对共享变量的写操作对其它线程的读操作可见,它是可见性与有序性的一套总结,抛开以下happens-before规则,JMM并不能保证一个线程对共享变量的写,对于其它线程对该共享变量的读可见

· 线程解锁m之前对变量的写,对于接下来对m加锁的其他线程对该变量的读可见

static int x;
static Object m = new Object();
new Thread(()->{
	synchronized(m){
		x = 10;
	}
},"t1").start();
new Thread(()->{
	synchronized(m){
		System.out.println(x);
	}
},"t2").start();

· 线程对volatile变量的写,对接下来其它线程对该变量的读可见

static int x;
static Object m = new Object();
new Thread(()->{
    synchronized(m){
        x = 10;
    }
},"t1").start();
new Thread(()->{
    synchronized(m){
        System.out.println(x);
    }
},"t2").start();

· 线程对volatile变量的写,对接下来其他线程对该变量的读可见

volatile static int x;
new Thread(()->{
    x = 10;
},"t1").start();
new Thread(()->{
    System.out.println(x);
},"t2").start();

· 线程start前对变量的写,对该线程开始后对该变量的读可见

static int x;
x = 10;
new Thread(()->{
	System.out.println(x);
},"t2").start();

· 线程结束前对变量的写,对其它线程得知它结束后的读可见(比如其它线程调用t1.isAlive()或t1.join()等待它结束)

static int x;
Thread t1 = new Thread(()->{
	x = 10;
},"t1");
t1.start();
t1.join();
System.out.println(x);

· 线程t1打断t2(interrupt)前对变量的写,对于其他线程得知t2被打断后对变量的读可见(通过t2.interrupted或t2.isInterrupted)

static int x;
public static void main(String[] args){
    Thread t2 = new Thread(()->{
        while(true){
            if(Thread.currentThread().isInterrupted()){
                System.out.println(x);
                break;
            }
        }
    },"t2");
    t2.start();
    new Thread(()->{
        sleep(1);
        x = 10;
        t2.interrupt();
    },"t1").start();
    while(!t2.isInterrupted()){
        Thread.yield();
    }
    System.out.println(x);
}

· 对变量默认值(0,false,null)的写,对其它线程对该变量的读可见。具有传递性,如果x hb-> y并且y hb-> z 那么有x hb-> z,配合volatile的防指令重排。

volatile static int x;
static int y;
new Thread(()->{
    y = 10;
    x = 20;
},"t1").start();
new Thread(()->{
    // x=20 对 t2可见,同时 y=10 也对t2可见
    System.out.println(x);
},"t2").start();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值