Java内存模型(JMM) 和 volatile 的讲解

一、JMM讲解

1、什么是JMM?

1.1、JMM的简介:
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念 并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

1.2、JMM关于同步规定:
a、线程解锁前,必须把共享变量的值刷新回主内存。
b、线程加锁前,必须读取主内存的最新值到自己的工作内存。
c、加锁解锁是同一把锁。

1.3、底层原理:
JVM运行程序的实体是线程,每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行;首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝。

2、JMM的底层访问过程图:

在这里插入图片描述

3、JMM的三大特点:

a、保证可见性。
b、保证原子性。
c、保证有序性。

二、volatile 讲解

1、什么是volatile?

volatile 是 Java 虚拟机提供的轻量级的同步机制,常被称为"轻量级的synchronized"。

2、volatile 的特点

a、保证可见性。
b、保证有序性(禁止指令重排)。
c、不保证原子性。

2.1、代码案例演示:

2.1.1 volatile可见性代码演示:

import java.util.concurrent.TimeUnit;

class MyData{

    boolean flag = false;          // 没有加volatile ,就没有可见性
    //volatile boolean flag = false;   // 加了volatile ,就有可见性

    public void modifyTrue(){
        flag = true;
    }
}

/**
 * 验证volatile的可见性
 */
public class VolatileDemoTest {
    public static void main(String[] args) {
        MyData myData = new MyData();

        System.out.println(Thread.currentThread().getName() + "\t 开始");
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t 线程进入");
            //暂停一会儿线程
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.modifyTrue();
            System.out.println(Thread.currentThread().getName() + "\t 线程修改结束, number = "+myData.flag);
        }, "AAA").start();

        // 第2个线程 main线程
        while (myData.flag == false){
            // main线程一致循环等待,直到flag不为false
        }

        System.out.println(Thread.currentThread().getName() + "\t 主线程结束number = "+myData.flag);
    }
}

a、当代码中 boolean flag = false; 前面没有加 volatile关键字的时候,程序没有退出;说明线程AAA将flag修改成了true,然后没有通知到主线程的flag,导致程序一直不退出。
在这里插入图片描述
b、当代码中 volatile boolean flag = false; 代码前面加了关键字volatile,当线程AAA将flag修改成了true时,将修改的结果通知主线程,主线程收到消息后,就退出循环,程序结束退出。
在这里插入图片描述
在这里插入图片描述

2.1.2、volatile不保证原子性代码演示:


class MyData{

    volatile int number = 0;

    // 此时number 前面加了volatile 关键字修饰,volatile不保证原子性
    public  void addPlus(){
        number ++;
    }
}

/**
 * 验证volatile不保证原子性
 * 1.原子性指的是什么意思:
 *      不可分割, 完整性, 也即某个线程正在做某个具体业务时,需要整体完整性
 *      要么同时成功,要么同时失败
 *
 * 4.如何解决
 *   4.1 加上synchronized,但是synchronized太重,杀鸡不用牛刀
 *   4.2 使用AtomicInteger(推荐)
 */
public class VolatileNoAtomic {

    public static void main(String[] args) {
        MyData myData = new MyData();

        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlus();
                }
            }, String.valueOf(i)).start();
        }

        //需要等待上面20个线程计算完成后,再用main取得最终结果
        while (Thread.activeCount() > 2){
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + "最终 volatile number:" +myData.number);
    }
}

a、运行结果:如果volatile保证原子性,那么运行结果应该是20*1000 等于20000 才是正确的。
在这里插入图片描述
在这里插入图片描述
b、那么如何保证原子性呢?
第一:可以使用加上synchronized关键字,但是不推荐,因为处理一件小的事情,用加上synchronized关键字重量级的锁,有点得不偿失。
第二:推荐使用AtomicInteger,AtomicInteger底层是通过CAS和Unsafe类来保证原子性的,没有实际加锁,而是使用了自旋锁的思想。

AtomicInteger 保证原子性代码演示:

import java.util.concurrent.atomic.AtomicInteger;

class MyData{

    volatile int number = 0;

    // 此时number 前面加了volatile 关键字修饰,volatile不保证原子性
    public  void addPlus(){
        number ++;
    }

    AtomicInteger atomicInteger = new AtomicInteger();
    public void addMyAtomic(){
        atomicInteger.getAndIncrement();
    }
}

/**
 * 验证volatile不保证原子性
 * 1.原子性指的是什么意思:
 *      不可分割, 完整性, 也即某个线程正在做某个具体业务时,需要整体完整性
 *      要么同时成功,要么同时失败
 *
 * 2.如何解决
 *   2.1 加上synchronized,但是synchronized太重,杀鸡不用牛刀
 *   2.2 使用AtomicInteger(推荐)
 */
public class VolatileNoAtomic {

    public static void main(String[] args) {
        MyData myData = new MyData();

        for (int i = 0; i < 20; i++) {
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlus();
                    myData.addMyAtomic();
                }
            }, String.valueOf(i)).start();
        }

        //需要等待上面20个线程计算完成后,再用main取得最终结果
        while (Thread.activeCount() > 2){
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + "最终 volatile number:" +myData.number);
        System.out.println(Thread.currentThread().getName() + "最终 AtomicInteger number:" +myData.atomicInteger);
    }
}

运行结果:使用volatie 修饰的不保证原子性,结果小于20000;使用AtomicInteger修饰的保证原子性,结果等于20000.
在这里插入图片描述

2.1.3、volatile 有序性(禁止指令重排)演示和总结:

public class SingletonDemo {
    private static volatile SingletonDemo instance = null;

    private SingletonDemo() {
        System.out.println(Thread.currentThread().getName() + "\t 构造方法");
    }
    /**
     * 双重检测机制
     *
     * @return
     */
    public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}
         

总结:
a、private static volatile SingletonDemo instance = null; instance 前面加了volatile可以禁止指令重排,比如代码中 instance = new SingletonDemo();这行代码,执行的顺序应该是:
1.为instance 分配内存空间;
2.初始化instance ;
3.将instance 指向分配的内存地址。
如果程序发生指令重排,顺序可能是1->3->2; 这样当对象被调用的时候,虽然对象不为空,但是没有初始化。

b、因此volatile可以禁止指令重排,保证程序在多线程环境下也可以安全运行。
c、详解:

DCL(双端检锁) 机制不一定线程安全,原因是有指令重排的存在,加入volatile可以禁止指令重排
原因在于某一个线程在执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化.
instance=new SingletonDem(); 可以分为以下步骤(伪代码)

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.初始化对象
但是指令重排只会保证串行语义的执行一致性(单线程) 并不会关心多线程间的语义一致性
所以当一条线程访问instance不为null时,由于instance实例未必完成初始化,也就造成了线程安全问题.

3、volatile 的示例

1、面试官:项目中有用到valotile吗?
答:一般用于 单例模式DCL代码 (即双重校验锁机制),如上面 2.1.3 的禁止指令重排的代码。

4、volatile关键字和synchronized 关键字比较

a、volatile关键字的性能要优于synchronized关键字,因为volatile关键字是线程同步的轻量级实现,而synchronized关键字是重量级的。
b、volatile关键字只能作用于变量;而synchronized关键字可以修饰方法和代码块。
c、多线程访问volatile关键字不会发生阻塞,而访问synchronized关键字可能会发生阻塞。
d、volatile关键字保证了数据的可见性,而不能保证数据的原子性;而synchronized关键字两者都可以。
e、volatile关键字主要用于解决变量在多个线程之间的可见性,而synchronized关键字主要用于解决多线程之间访问资源的同步性。

相关推荐
©️2020 CSDN 皮肤主题: 1024 设计师:白松林 返回首页