Java内存模型——JMM

概述

  • Java内存模型的定义是为了屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果
  • Java内存模型目的是定义程序中共享变量的访问规则,定义了一套多线程读写共享数据时,对数据的可见性、有序性和原子性的规则和保障

原子性

大致可以认为,基本数据类型的访问,读写都是具备原子性的(除了long、double这两个64位的类型,在运算时会被分成两个32位进行运算);如果要保证更大范围内的原子性,Java内存模型提供了lock和unlock操作,对应的字节码指令是monitorenter和monitorexit,对应的Java代码是sychronized关键字

sychronized

原理

如果锁住的是对象,则会通过monitorenter和monitorexit指令;如果锁住的是方法,则会通过ACC_SYCHRONIZED标识
在这里插入图片描述

案例

public class Main{
    static int num = 0;
    static Object obj = new Object();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            synchronized (obj){
                for (int i = 0; i < 1000; i++) {
                	// 自增不是一个原子操作,自增实际上是num = num+1,存在num+1和赋值两个操作
                    num++;
                }
            }
        });
        Thread t2 = new Thread(()->{
            synchronized (obj){
                for (int i = 0; i < 1000; i++) {
                    num++;
                }
            }
        });
        t1.start();
        t2.start();

        //等待綫程t1和t2
        t1.join();
        t2.join();
        System.out.println(num);
    }
}

可见性

主内存和工作内存

JMM规定所有变量都存储在主内存中,但是每个线程都有自己的工作内存
在这里插入图片描述
线程在运行过程中,会将需要用到的数据从主内存中加载到工作内存,此时对主内存的修改,工作内存不可见;工作内存中做的修改,对主内存也不可见

不可见现象

public class Main{
    static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(flag){
            }
        });
        t.start();
        Thread.sleep(1000);
        flag = false;
    }
}

上面这段代码理应在一秒过后退出,但是主线程对flag的修改对线程t不可见,所以会一直循环

当while循环中调用System.out.println()方法时,会发现程序退出了

public class Main{
    static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(flag){
                System.out.println();
            }
        });
        t.start();
        Thread.sleep(1000);
        flag = false;
    }
}
  • 原因是JVM会尽力去保证内存的可见性,即使没有同步关键字的存在;volatile的意义是强制性的保证可见性;在没有sout时,由于CPU一直在处理循环,CPU一直饱受占用,没有时间去处理别的事情,所以JVM不能强制要求CPU分点时间去主内存中取最新的变量值;而sout由于sychronized关键字,导致主内存中的数据倍同步到工作内存
  • JMM对sychronized有两条规定:线程解锁时,必须把贡献变量的最新值刷新到主内存中;线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量要从主内存中重新读取

可见性解决方案

  • 可见性除了上述使用sychronized关键字来实现,还可以通过volatile关键字,volatile关键字不会保证原子性,同时也是非阻塞的;所以在不需要保证变量操作的原子性时,使用volatile解决可见性性能更高
  • volatile可以保证每次工作线程访问变量都会从主内存去读取
public class Main{
    static volatile boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(()->{
            while(flag){
            }
        });
        t.start();
        Thread.sleep(1000);
        flag = false;
    }
}

有序性

指令重排序

public class Main{
    static boolean flag = false;
    static int num = 1;
    static int res = 0;
    
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            if(flag){
                res = num+num;
            }else{
                res = 1;
            }
        });
        Thread t2 = new Thread(()->{
            num = 2;
            flag = true;
        });
        t2.start();
        t1.start();

        t1.join();
        t2.join();
        System.out.println(res);
    }

}

这段代码经过分析,可能出现如下几种情况:

  1. t2先执行到flag=true,然后t1执行,进入if分支,得到res结果为4
  2. t1先执行,进入else分支,得到res结果为1
  3. 由于指令重排序,导致t2中res=true在num=2之前执行,他t1执行进入if分支,得到res结果为0

指令重排序:JIT编译器在运行时的一些优化,如果一段代码执行的先后顺序对执行结果不会有影响时,代码可能不会按照源码顺序进行执行;如上num的赋值和flag的赋值对线程t2的结果不会有影响,所以两者的顺序并不能保证num=2一定在flag=true前面,在单线程环境并不会有什么问题,但是多线程环境下,自己线程中代码的执行顺序可能会影响其他线程,最终造成线程安全问题

保证有序性

对变量使用volatile修饰,即可禁止操作该变量的指令重排序,如上对flag使用volatile修饰即可保证有序性

经典案例

double-checked locking的单例模式

public final class Singleton{
    public static void main(String[] args) {
        Singleton.getInstance();
    }
    private Singleton(){}
    private static Singleton instance = null;
    public static Singleton getInstance(){
        if(instance==null){
            synchronized (Singleton.class){
                if(instance==null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

在这里插入图片描述

  • 如果先执行了putstatic指令,instance就不为null,那么其他线程此时调用getInstance方法会直接拿到这个instance,而这个instance还没有执行完整的构造方法,导致线程安全问题
  • 对instance使用volatile修饰即可解决这个问题

happens-before(先行先发生)

happens-before规定了哪些写操作对其他线程的读操作可见,它是可见性和有序性的一套规则总结

  1. 线程解锁之前对变量的写,对于接下来对m加锁的其他线程对该变量的读可见
  2. 线程对volatile变量的写,对接下来其他线程对该变量的读可见
  3. 线程start前对变量的写,对该线程开始后对该变量的读可见
  4. 线程结束前对变量的写,对其他线程得知他结束后的读可见,如其他线程调用isAlive或join方法等待他结束
  5. 线程t1打断t2前对变量的写,对于其他线程得知t2被打断后的变量都可见,通过interrupted或isInterrrupted查看线程是否被打断
  6. 对变量默认值的写,对其他线程对该变量的值可见
  7. 具有传递性
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值