volatile是什么?

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

  1. 保证可见性
  2. 不能保证原子性
  3. 禁止指令重排

面试题:volatile是Java虚拟机提供的轻量级的同步机制,是基本上遵守了JMM的规范,主要是保证可见性和禁止指令重排,但是它并不保证原子性。

2. JMM

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

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

多个线程中从主内存拷贝值到各自线程的工作内存,若有一个线程改了当前拷贝过来的这个值,并将修改好的值返回给了主内存,
那么此时其他线程不知道这个线程已经把值改了,此时必须要有一种机制,只要有一个线程修改完自己的工作内存的值,并写回给主内存以后要及时通知其他线程,这样即使通知的这种情况,就是JMM内存模型里面第一个重要特性:俗称:可见性。

JMM模型需要保证:可见性、原子性、有序性

3. 可见性的验证

  1. 假如int number = 0; number变量之前没有添加volatile关键字修饰,没有可见性。
import java.util.concurrent.TimeUnit;

class MyData{
    int number = 0;
    public void addto60(){
        this.number = 60;
    }
}

public class VolatileDemo {
    public static void main(String[] args) {

        MyData myData = new MyData(); //线程操作资源类

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t come in~");
             //线程暂停3秒钟
            try{       TimeUnit.SECONDS.sleep(3);}catch(InterruptedException e){e.printStackTrace();}
            myData.addto60();
            System.out.println(Thread.currentThread().getName()+"\t update number value"+myData.number);
        },"AAA").start();

        //第二个线程就是我们的main线程
        while (myData.number==0){
		//main主线程就一直在这里等待循环,直到number不再等于零
        }

//若这句话打印出来了,说明main主线程感知到了number的值发生了变化,那么此时可见性就会被触发
System.out.println(Thread.currentThread().getName()+"\t"+myData.number);
    }
}
//输出,会卡在while循环处。
AAA	 come in~
AAA	 update number value60
  1. 在int number = 0之前添加volatile关键字修饰,volatile int number = 0;可以保证可见性。
import java.util.concurrent.TimeUnit;

class MyData{
    volatile int number = 0;
    public void addto60(){
        this.number = 60;
    }
}

public class VolatileDemo {
    public static void main(String[] args) {

        MyData myData = new MyData();

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"\t come in~");
            try{
                TimeUnit.SECONDS.sleep(3);}catch(InterruptedException e){e.printStackTrace();}
            myData.addto60();
            System.out.println(Thread.currentThread().getName()+"\t update number value"+myData.number);
        },"AAA").start();


        while (myData.number==0){

        }

        System.out.println(Thread.currentThread().getName()+"\t"+myData.number);
    }
}
//输出
AAA	 come in~
AAA	 update number value60
main	60

4. 不能保证原子性的例子

class MyData{
    volatile int number = 0;
    public void addto60(){
        this.number = 60;
    }

    public void addplusplus(){
        number++;
    }
}

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.addplusplus();
                }
            },String.valueOf(i)).start();
        }
        
        //需要等待上面20个线程都全部计算完成之后,再用main线程取得最终的结果值看是多少?
        while (Thread.activeCount()>2){ 
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName()+"\t"+myData.number);
    }
}
//输出,如果可以保证原子性,输出结果一定是20000
main	19101

JAVA内存模型(JMM)要求保证原子性,但是volatile是不保证原子性的

number++在多线程下是非线程安全的,如何不加synchronized解决?
在这里插入图片描述
可以使用java.util.concurrent.atomic包下的AtomicInteger(带原子性包装的整型类)来解决不保证原子性问题

class MyData{
    volatile int number = 0;
    public void addto60(){
        this.number = 60;
    }

    public void addplusplus(){
        number++;
    }

    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.addplusplus();
                    myData.addMyAtomic();
                }
            },String.valueOf(i)).start();
        }

        //需要等待上面20个线程都全部计算完成之后,再用main线程取得最终的结果值看是多少?
        while (Thread.activeCount()>2){
            Thread.yield();
        }

        System.out.println(Thread.currentThread().getName()+"\t"+myData.number);
        System.out.println(Thread.currentThread().getName()+"\t"+myData.atomicInteger);
    }
}
//输出
main	19492
main	20000

AtomicInteger这个玩意它的底层原理知道吗?

  • 答:知道,AtomicInteger底层是CAS

5. 指令重排

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

6. volatile的使用场景

单机版的单例模式:

public class SingletonDemo {
    private static SingletonDemo instance = null;
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"\t 我是构造方法SingletonDemo");
    }

    public static SingletonDemo getInstance(){
        if(instance==null){
            instance = new SingletonDemo();
        }
        return instance;
    }

    public static void main(String[] args) {
        System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
        System.out.println(SingletonDemo.getInstance()==SingletonDemo.getInstance());
    }
}

单例模式在多线程环境下可能会存在安全问题

单例模式DCL代码,可以保证多线程下的安全问题。如下所示:

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();
        }
    }
}
  • 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实例未必完成初始化,也就造成了线程安全问题.
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值