4.9--Java多线程之Java内存模型+volatile总结--(复习+总结篇)---一步一个脚印

Java内存模型(JMM)规定了线程如何访问共享变量,以解决并发编程中的原子性、可见性和有序性问题。volatile关键字能确保变量的内存可见性和禁止指令重排序,但不保证原子性。synchronized和Lock可以保证原子性和可见性。文章还讨论了volatile在单例模式和状态标志中的应用,以及其在多线程环境中的作用。
摘要由CSDN通过智能技术生成

1.什么是 JMM 内存模型:
Java 内存模型是 Java 虚拟机定义的一种多线程访问 Java 内存各个变量的访问规范,主要围绕如何解决并发过程中的原子性、可见性、有序性这三个问题来解决线程的安全问题

在这里插入图片描述

Java 内存模型将内存分为了主内存和工作内存(也称为栈空间)。
主内存存放所有的共享变量,所有线程都可以访问。
每个线程都有自己的工作内存,存储了该线程使用到的变量的副本,线程对变量的所有操作都必须在自己的工作内存中完成,不能直接操作主存中的变量。
操作时,首先将变量从主内存拷贝到自己的工作内存中,然后在自己的工作内存中对变量进行操作,操作完成后再将变量写回主存。
不同的线程间也无法直接访问对方的工作内存的变量,线程间的变量值的传递必须通过主内存来完成。

1.原子性:原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。
2.可见性:可见性指的是,当一个线程修改了某个共享变量的值,其他线程能够马上得知这个修改的值。

串行程序不存在可见性问题,因为在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个修改过的变量值。
但在多线程环境中可就不一定了,因为线程对共享变量的操作都是拷贝到各自的工作内存中进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题

3.有序性:对于多线程环境,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,有可能出现乱序现象

指令重排序:计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,在单线程条件下,指令重排序可以保证执行结果的一致性,但是在多线程条件下,这些重排优化可能会导致程序出现内存可见性问题,不能保证多线程间语义一致性

2、原子性、可见性、有序性问题的解决措施:

1.原子性问题:除了 JVM 自身提供对基本数据类型读写操作的原子性外,对于方法级别或者代码级别的原子性操作,可以使用 synchronized 关键字或者重入锁 ReentrantLock 保证程序执行的原子性。

2.可见性问题:工作内存与主内存同步延迟现象导致的可见性问题,可以使用 synchronized 关键字、Lock 或者 volatile 关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。

3.有序性问题:可以利用 volatile、synchronized 关键字解决。

3、JMM 的 as-if-serial 规则和 happens-before 规则:

1.as-if-serial:无论编译器和处理器如何进行重排序,单线程程序的执行结果不会改变。

2.happens-before:在多线程程序开发中,如果仅靠 synchronized 和 volatile 关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,所以 Java 内存模型中提供了内置的 happens-before 规则来辅助处理前面提到的问题,它是判断数据是否存在竞争、线程是否安全的依据,从而保证线程安全。

一个操作 happens-before 另一个操作,表示第一个的操作结果对第二个操作可见,并且第一个操作的执行顺序也在第二个操作之前。
但这并不意味着 JVM 必须按照这个顺序来执行程序。如果重排序后的执行结果与按 happens-before 关系执行的结果一致,JVM 也允许重排序的发生。
happens-before 原则内容如下:

1.程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
2.锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
3.volatile 规则:volatile 变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile 变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
4.线程启动规则:线程的 start() 方法先于它的每一个动作,即如果线程A在执行线程B的 start() 方法之前修改了共享变量的值,那么当线程B执行 start() 方法时线程A对共享变量的修改对线程B可见。
5.传递性:A先于B ,B先于C,那么A必然先于C
6.线程终止规则:线程的所有操作先于线程的终结,Thread.join() 方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程从线程B的 join() 方法成功返回后,线程B对共享变量的修改将对线程A可见。
7.线程中断规则:对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted() 方法检测线程是否中断。
8.对象终结规则:对象的构造函数执行,结束先于 finalize() 方法。

二、volatile 内存语义:

1、volatile 的作用:

volatile 是 Java 虚拟机提供的轻量级同步机制,是线程不安全的,volatile 跟可见性和有序性有关,被 volatile 修饰的共享变量,具有以下两个作用:

1.保证不同线程对该变量操作的内存可见性:当变量被 volatile 修饰时,那么对它的修改会立刻刷新到主存,同时其他线程操作 volatile 变量时,JMM 会把该线程对应的工作内存置为无效,那么该线程就只能从主存中重新读取共享变量,保证读取到最新值

通过 synchronized 和 Lock 也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是相比于 volatile,synchronized 和 Lock 的开销都更大

2.禁止指令重排序

2、内存屏障:

把加入 volatile 关键字的代码和未加入 volatile 关键字的代码都生成汇编代码,会发现加入 volatile 关键字的代码会多出一个内存屏障指令,它是一个 CPU 指令

JMM是如何实现让volatile变量对其他线程立即可见的呢?

当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程将只能从主内存中重新读取共享变量。volatile变量正是通过这种写-读方式实现对其他线程可见(但其内存语义实现则是通过内存屏障)

内存屏障提供了以下功能:

1.告诉编译器和处理器,重排序时不能把后面的指令重排序到内存屏障之前的位置,从而避免多线程环境下出现乱序执行现象

2.保存某些变量的内存可见性,利用该特性实现 volatile 的内存可见性

3、volatile 的原子性:
volatile 的两点内存语义能保证可见性和有序性,但是不能保证原子性:对单个 volatile 变量的读/写具有原子性,但是对于类似 volatile++ 这样的复合操作就无能为力了

要想保证原子性,只能借助于 synchronized、Lock 或者并发包下的 atomic 的原子操作类

4、volatile使用场景:
(1)状态量标记:这种对变量的读写操作,标记为 volatile 可以保证修改对线程立刻可见,效率也比 synchronized、Lock 有一定的提升。

public class VolatileSafe {
​
    volatile boolean close;
​
    public void close(){
        close=true;
    }
​
    public void doWork(){
        while (!close){
            System.out.println("safe....");
        }
    }
}

对于 boolean 变量 close 值的修改属于原子性操作,因此可以通过使用 volatile 修饰变量 close,使用该变量对其他线程立即可见,从而达到线程安全的目的。

(2)单例模式的实现,典型的双重检查锁定(DCL):

这是一种懒汉的单例模式,使用时才创建对象,而且为了避免初始化操作的指令重排序,给 instance 加上了 volatile。指令重排序会导致,当一条线程访问的 instance 不为 null 时,但是实际上 instance 实例未必已初始化完成,也就造成了线程安全问题

public class DoubleCheckLock {
    //禁止指令重排优化
    private volatile static DoubleCheckLock instance;
​
    private DoubleCheckLock(){}
    public static DoubleCheckLock getInstance(){
​
        //第一次检测
        if (instance==null){
            //同步
            synchronized (DoubleCheckLock.class){
                if (instance == null){
                    //多线程环境下可能会出现问题的地方
                    instance = new DoubleCheckLock();
                }
            }
        }
        return instance;
    }
}

上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)

memory = allocate(); //1.分配对象内存空间
instance(memory);    //2.初始化对象
instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null

由于步骤2和步骤3间可能会重排序,如下:

memory = allocate(); //1.分配对象内存空间
instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory);    //2.初始化对象

使用volatile禁止instance变量被执行指令重排优化即可。

  //禁止指令重排优化
  private volatile static DoubleCheckLock instance;

单例模式的线程安全性:某个类的实例在多线程环境下只会被创建一次出来。单例模式有很多种的写法:
(1)饿汉式单例模式的写法:线程安全
(2)懒汉式单例模式的写法:非线程安全
(3)双检锁单例模式的写法:线程安全

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值