谈谈你对volatile的理解

1.volatile是Java虚拟机提供的轻量级的同步机制

1.1保证可见性
1.2不保证原子性
1.3禁止指令重排

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

JMM关于同步规定:

1.线程解锁前,必须把共享变量的值刷新回主内存

2.线程加锁前,必须读取主内存的最新值到自己的工作内存

3.加锁解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:

2.1可见性

通过前面对JMM的介绍,我们知道

各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的.

这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享变量X对线程B来说并不可见.这种工作内存与主内存同步延迟现象就造成了可见性问题.volatile可以保证可见性,及时通知其它线程,主物理内存的值已经被修改

case

class MyData {
    int num = 0;
//    volatile int num = 0;

    public void addTo60() {
        this.num = 60;
    }
}

/**
 * 1、验证volatile的可见性
 *  1.1假如int num=0; num变量之前没有添加volatile关键字修饰,main线程死循环等待,程序无法结束
 *  1.2 num变量之前添加volatile关键字修饰,及时通知其它线程,main线程感知到修改,结束程序
 */
public class VolatileDemo {

    public static void main(String[] args) {
        MyData myData = new MyData();
        // 线程AAA
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " 进来了...");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.addTo60();
            System.out.println(Thread.currentThread().getName() + " 修改num为:" + myData.num);
        }, "AAA").start();

        while (myData.num == 0) {

        }

        // main线程
        System.out.println(Thread.currentThread().getName() + "感知到变量被修改...");
    }

}

2.2原子性

case

class MyData {
    volatile int num = 0;

    public void addTo60() {
        this.num = 60;
    }

    public void addAdd() {
        this.num++;
    }
}

/**
 * 2、验证volatile不保证原子性
 *  *  2.1 不保证原子性案例
 */
public static void main(String[] args) {
//        seeOkByVolatile();
        MyData myData = new MyData();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myData.addAdd();
                }
            }, String.valueOf(i)).start();
        }

        // 需要等待上面完成全部计算,看看main线程最后得到的结果是多少
        while (Thread.activeCount() > 2) {
            Thread.yield(); // 如果线程数大于2,就让出执行权.这里一个main线程,一个是后台GC线程
        }
        System.out.println(Thread.currentThread().getName() + "int type finally num = " + myData.num);
    }

num++在多线程下是非线程安全的,如何不加synchronized解决?

2.3解决原子性问题

使用java的JUC并发包下的原子操作类,其原理见CAS

import java.util.concurrent.atomic.AtomicInteger;

class MyData {
    volatile int num = 0;

    public void addTo60() {
        this.num = 60;
    }

    public void addAdd() {
        this.num++;
    }

    AtomicInteger atomicInteger = new AtomicInteger();

    public void addMyAtomic() {
        atomicInteger.getAndIncrement();
    }
}

public class VolatileDemo {

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

        // 需要等待上面完成全部计算,看看main线程最后得到的结果是多少
        while (Thread.activeCount() > 2) {
            Thread.yield(); // 如果线程数大于2,就让出执行权.这里一个main线程,一个是后台GC线程
        }
        System.out.println(Thread.currentThread().getName() + "int type finally num = " + myData.num);
        System.out.println(Thread.currentThread().getName() + "int type finally num = " + myData.atomicInteger);
    }
}

2.4有序性

计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下3种

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致.

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

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

重排1

public void mySort(){
    int x=11;//语句1
    int y=12;//语句2
    x=x+5;//语句3
    y=x*x;//语句4
}
1234
2134
1324
问题:
请问语句4 可以重排后变成第一条码?
存在数据的依赖性 没办法排到第一个

重排2

案例

3.你在哪些地方用到过volatile?

3.1 单例模式DCL代码

public class SingletonDemo {

    private static volatile SingletonDemo instance=null;
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"	 构造方法");
    }

    /**
     * 双重检测机制
     * @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();
        }
    }
}

3.2代理模式volatile分析

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实例未必完成初始化,也就造成了线程安全问题.

原文地址:https://www.cnblogs.com/weianlai/p/14589509.html