Java内存模型

内存模型

1. 可见性

1.1 问题引发

  • 一个线程对主存中数据写操作,对其他线程的读操作不可见
package com.erick.multithread.d4;

import java.util.concurrent.TimeUnit;

public class Demo01 {
    private static boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (flag) {
                /*中间不要打印任何东西,避免出现错误*/
            }
        }, "t1").start();

        TimeUnit.SECONDS.sleep(3);
        /*3秒后主线程修改了flag,但是上面线程没有如期停下来*/
        flag = false;
    }
}

1.2 原理分析

# 初始状态
- 初始状态,静态变量run被加载到主存中
- t线程从主存中读取到了run
- t线程要频繁从主存中读取数据,
  JIT编译器会将run的值缓存到自己的工作内存中的高速缓存中,减少对主存的访问,提高性能
 
 # 3秒后
- 主存中的数据被修改了,但是t线程还是读取自己工作内存的数据,并不会去主存中去拿

image-20221005104014090

1.3 解决方案

volatile
- 修饰成员变量或者静态成员变量
- 线程获取该变量时,不会从自己的工作内存中去读取,每次都是去主存中读取
- 牺牲了性能,保证了一个线程改变主存中某个值时,对于其他线程不可见的问题
package com.erick.multithread.d4;

import java.util.concurrent.TimeUnit;

public class Demo01 {
    private static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (flag) {
            }
        }, "t1").start();

        TimeUnit.SECONDS.sleep(3);
        flag = false;
    }
}
synchronized
- 解决不可见性:重量级锁
- java内存模式中,synchronized规定,线程在加锁时

1. 先清空工作内存
2. 在主存中拷贝最新变量到工作内存中
3. 执行代码
4. 将更改后的共享变量的值刷新到主存中
5. 释放互斥锁
package com.erick.multithread.d4;

import java.util.concurrent.TimeUnit;

public class Demo02 {
    private static boolean flag = true;
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (true){
                synchronized (lock){
                    if (!flag) {
                        break;
                    }
                }
            }
        }).start();

        TimeUnit.SECONDS.sleep(3);
        flag = false;
    }
}

1.4 犹豫模式–Balking

  • 一个线程发现另一个线程已经做好了某个事,那么该线程就无需再做
  • 类比单例模式
重复做
package com.erick.multithread.d4;

import lombok.Getter;

import java.util.concurrent.TimeUnit;

public class Demo03 {
    public static void main(String[] args) throws InterruptedException {
        Balking balking = new Balking();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> balking.operation()).start();
        }

        TimeUnit.SECONDS.sleep(3);
        System.out.println(balking.getJobDoneTimes());
    }
}

class Balking {
    private boolean isJobDone;
    @Getter
    private int jobDoneTimes;

    public void operation() {
        if (isJobDone) {
            return;
        }
        /*大概为5*/
        jobDoneTimes++;

        try {
            System.out.println(Thread.currentThread().getName() + " execute job");
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        isJobDone = true;
    }
}
synchronized
package com.erick.multithread.d4;

import lombok.Getter;

import java.util.concurrent.TimeUnit;

public class Demo04 {
    public static void main(String[] args) throws InterruptedException {
        BalkingJob balkingJob = new BalkingJob();
        for (int i = 0; i < 5; i++) {
            new Thread(() -> balkingJob.operation()).start();
        }

        TimeUnit.SECONDS.sleep(2);
        System.out.println(balkingJob.getJobDoneTimes());
    }
}

class BalkingJob {
    private boolean isJobDone;
    @Getter
    private int jobDoneTimes;

    public synchronized void operation() {
        if (isJobDone){
            return;
        }
        jobDoneTimes++;
        System.out.println(Thread.currentThread().getName() + " execute job");
        isJobDone = true;
    }
}

2. 有序性

2.1 计组思想

- 每条指令划分为    取指令 --- 指令译码 --- 执行指令 --- 内存访问 --- 数据回写
- 计组思想 并不能提高单个指令的执行时间,但是变相的提高了吞吐量

在这里插入图片描述
在这里插入图片描述

2.2 指令重排

  • 一条代码,可能被字节码执行分为若干个指令
  • JVM在不影响性能的情况下,会对代码的执行顺序进行重排
正常重排
  • 即使重排序,不会对结果产生影响
private int i;
private int j;

i=0;
j=2;
异常重排
  • 不可重排,重排后就会发生错误
  • 这种JVM层面的指令重排,就会引发莫名其妙的错误
private int i = 0;
private int j = 2;

i = 10;
j = i-1;
禁止重排
  • volatile修饰成员变量或者静态成员变量,该变量上面的所有指令都不会重排
private int i = 0;
private volatile int j = 1;

// 只需要在j上加,这样代码中j的上面的所有代码,就不会被放到j后实现
i = 10;
j = i - 1;

Volatile 原理

- Memory Barrier(Memory Fence):     底层实现是内存屏障
- 写屏障:                           在写操作后,加上写屏障
- 读屏障:                           在读操作前,加上读屏障

1. 可见性

1.1 写屏障

  • voliate修饰的变量,会在写操作后,加上写屏障( 代码执行处)
  • 在该屏障之前的所有代码的改动,同步到主存中去
package com.dreamer.multithread.day02;

public class Demo03 {

    private static int number = 0;

    private static volatile int age = 0;

    public static void main(String[] args) {

        new Thread(() -> {
            number++;
            age=10;
            // JVM  会加上写屏障
        }).start();

    }
}

1.2 读屏障

  • volatile修饰的变量,会在读取到该变量的前面,加上读屏障
  • 读屏障后面的数据,都会在主存中获取
package com.dreamer.multithread.day02;

public class Demo03 {

    private static int number = 0;

    private static volatile int age = 0;

    public static void main(String[] args) {
        
        new Thread(() -> {
            int ageResult = age;
            int numberResult = number;
        }).start();

    }
}

2. 有序性-指令重排

- 只能保证   本线程内   不会发生指令重排

2.1 写屏障

  • 确保指令重排时,写屏障之前的代码,不会重排到写屏障后面
private  int number = 0;
private volatile boolean flag = true;

public void method(){
     number ++;
     flag = false;   // 写屏障,不会将写屏障前面的代码,重排到写屏障后面
}

2.2 读屏障

  • 确保指令重排时,读屏障后的代码,不会重排到读屏障之前

3. 指令交错

- volatile不能保证不同线程在执行的时候的指令交错引发的问题(不能保证线程安全)
- 只是保证各个线程在执行的时候都从主存中去加载

4. synchronized/volatile

synchronizedvolatile
锁级别重量级锁轻量级锁
可见性可解决可解决
原子性(多线程指令交错)可解决不保证
指令重排可以重排,但不会出错可以禁止重排
场景一个线程修改,其他线程读多线程并发修改

Happen-Before原则

- 对共享变量的读写操作,代码的可见性和有序性的一套总结

1. synchronized

  • 线程解锁之前对变量的写,对于接下来的用加锁的其他线程的读,改变是可见的
  • 线程解锁前,会将写操作的变量,从工作内存会同步到主内存中
package com.dreamer.multithread.day02;

public class Demo04 {
    private static int x = 0;

    private static Object lock = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock) {
                x = 10;
            }
        }).start();

        new Thread(() -> {
            synchronized (lock) {
                System.out.println(x);
            }
        }).start();
    }
}

2. volatile

  • 变量用volatile修饰,一个线程对其的写操作,对于其他线程来说是可见的
  • 写屏障结合读屏障,保证是从主存中读取变量
package com.dreamer.multithread.day02;

public class Demo05 {
    private static volatile int x = 0;

    public static void main(String[] args) {
        new Thread(() -> x = 10).start();

        new Thread(() -> System.out.println(x)).start();
    }
}

3. 先写先得

  • 线程start前对变量的写操作,对该线程开始后的读操作是可见的
package com.dreamer.multithread.day02;

public class Demo06 {
    private static int x = 0;

    public static void main(String[] args) {

        x = 10;

        new Thread(() -> System.out.println(x)).start();
    }
}

4. 通知准则

  • 线程结束前对变量的写操作,对其他线程得知它结束后的读操作可见性
  • 如调用join方法
package com.dreamer.multithread.day02;

public class Demo07 {

    private static int x = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> x = 10);
        thread.start();

        thread.join();

        System.out.println(x);
    }
}

5. 打断规则

  • 线程t1 打断t2前,t1对变量的写,对于其他线程得知得知t2被打断后,对变量的读可见
package com.dreamer.multithread.day06;

public class Demo07 {
    private static int x = 0;

    public static void main(String[] args) {
        Thread t2 = new Thread("t2") {
            @Override
            public void run() {
                while (true) {
                    if (Thread.currentThread().isInterrupted()) {
                        break;
                    }
                }
            }
        };

        Thread t1 = new Thread("t1") {
            @Override
            public void run() {
                x = 5;
                t2.interrupt();
            }
        };

        t1.start();
        t2.start();

        while (t2.isInterrupted()) {
            Thread.yield();
        }

        System.out.println(x);
        
    }
}

6. 默认值

  • 对变量默认值的写,0,false,null,其他线程对该变量可见

7. 传递性

  • 其实就是volatile的读屏障和写屏障问题
package com.dreamer.multithread.day02;

public class Demo08 {
    private static int x = 0;

    private static volatile int y = 0;

    public static void main(String[] args) {
        new Thread(() -> {
            x = 10;
            y = 20;
            // 写屏障,会将上面的操作全部赋值到主存中去
        }).start();

        new Thread(() -> {
            System.out.println(x);
            System.out.println(y);
        }).start();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值