天天看点

JMM详解与volatile关键字一、Java内存模型——JMM二、volatile保证可见性三、volatile不保证原子性四、volatile禁止指令重排五、多线程下单例模式的安全问题

一、Java内存模型——JMM

JMM本身是一种抽象的概念,并不真实存在,它描述的是一组规则或者规范,就想JVM一样

通过JMM规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式

JMM的可见性、原子性、有序性使得线程安全得到保证

其中JMM对同步做出了规定:

  • 线程加锁前,必须读取主内存的最新值到自己的工作内存
  • 线程解锁前,必须把共享变量的值刷新回主内存
  • 加锁解锁必须是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存,工作内存是每个线程的私有数据区域(类似于JVM在运行时数据数据区中为每一个线程都分配一份线程私有的PC、虚拟机栈、本地方法栈)。而JMM中规定所有变量都储存在主内存,主内存是共享内存区域,所有线程都可以访问(类似于JVM运行时数据区中的堆空间和方法区,所有线程共享数据,都可以访问)。

但线程对变量的读写等操作必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存,然后再对变量进行读写操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量。各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问其他线程的工作内存,线程间的通信必须通过主内存来完成

JMM详解与volatile关键字一、Java内存模型——JMM二、volatile保证可见性三、volatile不保证原子性四、volatile禁止指令重排五、多线程下单例模式的安全问题

二、

volatile

保证可见性

由于JMM规范,各个线程对主内存中的共享变量的操作都是各个线程各自拷贝到自己的工作内存中进行操作后再写回到主内存中,这就导致了各个线程之间的不可见性问题,各个线程之间需要同步机制。

volatile

是JVM提供的轻量级同步机制

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

接下来验证

volatile

的可见性

代码中的

number

变量没有被

volatile

关键字修饰,

new Thread

第一个线程将

number

值改为

10

,由于各个线程之间无法互相访问,所以

main

线程并不知道

number

值已经被修改,

main

线程拿到的

number

变量值还是刚开始时从主内存中拷贝到自己工作内存中值

,由于不可见性导致一直在循环中出不来

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

        //创建第一个线程
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 开始执行");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addNumber();
            System.out.println(Thread.currentThread().getName() + "更新变量number:" + myData.number);
        }, "Thread1").start();

        //第二个线程 main线程
        while (myData.number == 0) {

        }

        System.out.println(Thread.currentThread().getName() + "main线程执行完成:number:" + myData.number);

    }
}

class MyData {
    int number = 0;

    public void addNumber() {
        this.number = 10;
    }
}
           

输出结果

JMM详解与volatile关键字一、Java内存模型——JMM二、volatile保证可见性三、volatile不保证原子性四、volatile禁止指令重排五、多线程下单例模式的安全问题

number

使用

volatile

修饰,

new Thread

第一个线程改变

number

值后,会通知

main

线程主内存的值已被修改,体现出

volatile

关键字保证了可见性

JMM详解与volatile关键字一、Java内存模型——JMM二、volatile保证可见性三、volatile不保证原子性四、volatile禁止指令重排五、多线程下单例模式的安全问题

输出结果

JMM详解与volatile关键字一、Java内存模型——JMM二、volatile保证可见性三、volatile不保证原子性四、volatile禁止指令重排五、多线程下单例模式的安全问题

三、

volatile

不保证原子性

原子性是指保证数据整体的完整性,某个线程在执行时,中间不可被加塞或分割,需要整体完整,要么同时成功,要么同时失败。

volatile

不保证原子性,同时

addPlusPlus()

方法也不是同步方法,多线程的情况下,各个线程修改完各自从主内存拷贝的

number

值之后,在写回主内存的时候可能会发生覆盖,导致部分写操作无效

public static void main(String[] args) {
        MyData myData = new MyData();
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus();
                }
            }, String.valueOf(i)).start();
        }

        //等待上面20个线程全部计算结束
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + " finally number is " + myData.number);
    }
}
class MyData {
    volatile int number = 0;

    public void addNumber() {
        this.number = 10;
    }

    public void addPlusPlus() {
        this.number++;
 }
           

输出结果

JMM详解与volatile关键字一、Java内存模型——JMM二、volatile保证可见性三、volatile不保证原子性四、volatile禁止指令重排五、多线程下单例模式的安全问题

从JVM字节码指令角度分析,

number++

操作在多线程下是非线程安全的

JMM详解与volatile关键字一、Java内存模型——JMM二、volatile保证可见性三、volatile不保证原子性四、volatile禁止指令重排五、多线程下单例模式的安全问题

那如何解决

volatile

不保证原子性的问题?有两种解决方法

  • 使用

    JUC

    包提供的

    Atomic

    原子类
  • 使用

    synchronized

    关键字保证方法同步

使用

Atomic

原子类保证原子性

public static void main(String[] args) {
        MyData myData = new MyData();
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus();
                    myData.addMyAtomicInteger();
                }
            }, String.valueOf(i)).start();
        }

        //等待上面20个线程全部计算结束
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName() + " int类型 finally number is " + myData.number);
        System.out.println(Thread.currentThread().getName() + " AtomicInteger类型 finally number is " + myData.myAtomicInteger);
    }

class MyData {
    volatile int number = 0;

    public void addNumber() {
        this.number = 10;
    }

    public void addPlusPlus() {
        this.number++;
    }

    AtomicInteger myAtomicInteger = new AtomicInteger(0);

    public void addMyAtomicInteger() {
        myAtomicInteger.getAndIncrement();
    }
}
           

输出结果

JMM详解与volatile关键字一、Java内存模型——JMM二、volatile保证可见性三、volatile不保证原子性四、volatile禁止指令重排五、多线程下单例模式的安全问题

使用同步关键字

synchronized

保证原子性,使用同步代码块或者使用同步方法

JMM详解与volatile关键字一、Java内存模型——JMM二、volatile保证可见性三、volatile不保证原子性四、volatile禁止指令重排五、多线程下单例模式的安全问题
JMM详解与volatile关键字一、Java内存模型——JMM二、volatile保证可见性三、volatile不保证原子性四、volatile禁止指令重排五、多线程下单例模式的安全问题

四、

volatile

禁止指令重排

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,可能会导致多线程程序出现内存可见性问题

JMM详解与volatile关键字一、Java内存模型——JMM二、volatile保证可见性三、volatile不保证原子性四、volatile禁止指令重排五、多线程下单例模式的安全问题

处理器在进行重排序时必须考虑指令之间的数据依赖性

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果也就无法预测

处理器基本上都是使用写缓冲区临时保存向主内存写入的数据,虽然写缓冲区保证了处理器的性能,但是每个处理器上的写缓冲区仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生影响,也就是说处理器对内存的读写操作的执行顺序,不一定与内存实际发生的读写操作顺序一致!关键原因在于有写缓存区的存在,并且仅对自己的处理器可见。

如下图所示,从内存操作实际发生的顺序来看,知道处理器

A

执行

A3

来刷新自己的写缓存区里的数据到主内存中,写操作

A1

才算真正执行了,虽然处理器

A

执行内存操作的顺序是

A1->A2

,但是内存实际操作的顺序

A2->A1

。此时处理器

A

的内存操作顺序被重排序了

JMM详解与volatile关键字一、Java内存模型——JMM二、volatile保证可见性三、volatile不保证原子性四、volatile禁止指令重排五、多线程下单例模式的安全问题

volatile

实现了禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象

底层实现是因为内存屏障指令,又称内存栅栏,是一个CPU指令。为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。这个指令可以保证特定操作的执行顺序,也保证某些变量的内存可见性(利用该特性实现

volatile

的内存可见性)。

由于编译器和处理器都能执行指令重排优化,如果在指令间插入一条

Memory Barrier

则会告诉编译器和处理器,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说,通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

JMM详解与volatile关键字一、Java内存模型——JMM二、volatile保证可见性三、volatile不保证原子性四、volatile禁止指令重排五、多线程下单例模式的安全问题

五、多线程下单例模式的安全问题

单例模式的DCL(双重检锁)模式,虽然加了同步关键字,但是多线程下依然会有线程安全问题

public class SingletonDemo {
    private static SingletonDemo singletonDemo=null;
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"\t 我是构造方法");
    }
    //DCL模式 Double Check Lock 双端检索机制:在加锁前后都进行判断
    public static SingletonDemo getInstance(){
        if (singletonDemo==null){
            synchronized (SingletonDemo.class){
                 if (singletonDemo==null){
                     singletonDemo=new SingletonDemo();
                 }
            }
        }
        return singletonDemo;
    }

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

其中

singletondemo=new SingletonDemo()

创建一个单例对象可分为以下3步:

memory = allocate(); //1.分配内存
singletondemo(memory);	 //2.初始化对象
singletondemo= memory;	 //3.设置引用地址
           

初始化对象和设置引用地址没有数据依赖关系,可能发生指令重排。

如果发生指令重排,那么在第一次检测,读取到的

singletondemo

不为

null

时,

singletondemo

的引用对象有可能还没有完成初始化,两次检测都会跳过,返回一个对象还没有初始化完成的引用,导致线程安全问题

解决上述问题的方法,可以给

singletondemo

对象添加上

volatile

关键字,禁止指令重排