volatile的深度理解

目录

1.什么是volatile

2.volatile三大特性

3.JMM(Java内存模型) 

 3.1JMM要求有三大特性

3.1.1可见性

3.1.2原子性

3.1.3有序性

4.volatile的使用


1.什么是volatile

        volatile是Java中的关键字,用来修饰会被不同线程访问和修改的变量。

        volatile是一种轻量级的同步机制。在原子性方面它仅能保障写 volatile 变量操作的原子性,但没有锁的排他性;volatile 关键字的使用不会引起上下文切换(所以被称为 “轻量级” )。

2.volatile三大特性

1.保证可见性

2.不保证原子性

3.禁止指令重排

3.JMM(Java内存模型) 

        要想深度理解volatile,要先知道JMM什么。

        JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这绞规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
        JMM关于同步的规定:
        1 线程解锁前,必须把共享变量的值刷新回主内存;
        2 线程加锁前,必须读取主内存的最新值到自己的工作内存;
        3 加锁解锁是同一把锁。

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

 3.1JMM要求有三大特性

3.1.1可见性

        出现不满足可见性问题的原因

        通过前面对JMM的介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。
        这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题。

        定义:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

        在多线程环境下,一个线程对共享变量的操作对其他线程是不可见的。Java提供了volatile来保证可见性,当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后他会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。当然,synchronize和Lock都可以保证可见性。synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。        

        不加volatile,代码如下:

import java.util.concurrent.TimeUnit;

class MyData{
    int number=0;
    public void addTo60(){
        number+=60;
    }
}
/**
 * 验证volatile的可见性
 *    假如 int number=0; ,number之前没有添加volatile关键字修饰,没有可见性
 */
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();

        //第二个线程,main线程
        while (myData.number==0){
            //main线程就一直在这里等待循环,知道number值不在等于0
        }
        System.out.println(Thread.currentThread().getName()+"\t mission is over");
    }
}

        结果如下:

         加volatile,main方法不变(同上),代码如下:

class MyData{
    volatile int number=0;
    public void addTo60(){
        number+=60;
    }
}

        结果如下:

        以上就是对volatile的可见性证明。

3.1.2原子性

         出现不满足原子性问题的原因

        Java中代码运行,将.java文件编译为.class文件,虚拟机运行字节码文件,在Java文件中的一条代码,编译之后可能是多条语句,当这些语句执行一半的时候,其他线程抢占了CPU资源,去执行的语句对你里面的数据有影响,这就是原子性问题。

        定义: 即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

        原子性是拒绝多线程操作的,不论是多核还是单核,具有原子性的量,同一时刻只能有一个线程来对它进行操作。简而言之,在整个操作过程中不会被线程调度器中断的操作,都可认为是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。Java中的原子性操作包括:

(1)基本类型的读取和赋值操作,且赋值必须是值赋给变量,变量之间的相互赋值不是原子性操作。

(2)所有引用reference的赋值操作

(3)java.concurrent.Atomic.* 包中所有类的一切操作

        代码示例:

class MyData{
    volatile int number=0;

    //请注意,前面number前面加了volatile关键字修饰,volatile不保证原子性
    public synchronized void addPlusPlus(){
        number++;
    }
}

/**
 * 2.验证volatile不保证原子性
 *    2.1 原子性指的是什么意思?
 *      不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以加塞或者被分割。需要整体完整
 *      要么同时成功,要么同时失败
 *    2.2 volatile 不保证原子性的案例
 */
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){//只有main线程和gc线程跳出循环
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t finally number value:"+myData.number);
    }

}

        运行结果:

         基本上number的值不会等于20000,证明了volatile修饰的变量不能保证原子性。

        如何保证原子性?

        1.向对变量操作的方法中加上synchronized,使其变成同步方法,但效率太低了,不推荐。

        2.使用Java并发包中的原子操作类,如AtomicInteger类,代替int,推荐使用。

        使用AtomicInteger类,代码如下:

import java.util.concurrent.atomic.AtomicInteger;

class MyData{
    volatile int number=0;

    //请注意,前面number前面加了volatile关键字修饰,volatile不保证原子性
    public void addPlusPlus(){
        number++;
    }

    AtomicInteger atomicInteger=new AtomicInteger();
    public void addAtomic(){
        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.addAtomic();
                }
            },String.valueOf(i)).start();
        }
        //需要等待上面20个线程都全部计算完后,在用main线程取得最终结果值是多少?
        while (Thread.activeCount()>2){//只有main线程和gc线程跳出循环
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t int type,finally number value:"+myData.number);
        System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type,finally atomicInteger value:"+myData.atomicInteger);
    }

}

        结果如下:

3.1.3有序性

         出现不满足有序性问题的原因

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

        在单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
        处理器在进行重排序时必须要考虑指令之间的数据依赖性。
        多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

       在单线程下例子1,有如下代码:

    public void mySort(){
        int x=1;//语句1
        int y=2;//语句2
        x+=5;//语句3
        y=x*x;//语句4
    }

        经过指令重排后,代码执行的顺序可能是1234,或2134,或1324。

        在多线程下例子2:

        在多线程下例子3:

定义:即程序执行的顺序按照代码的先后顺序执行。 

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
 先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:一是保证特定操作的执行顺序,
二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

4.volatile的使用

1)单一赋值可以,but含复合运算赋值不可以(i++之类)
如:volatile int a=10;

        volatile boolean flag=false;

2)状态标志,判断业务是否结束

3)开销较低的读,写锁策略

4)单例模式懒汉式在多线程情况下可能存在的线程安全问题

        不加volatile的懒汉式双端检锁机制代码:

public class SingletonDemo {
    private static SingletonDemo singletonDemo;
    private SingletonDemo(){
        System.out.println("单例下构造器执行一次");
    }
    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)).start();
        }
    }
}

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

instance = new SingletonDemo();可以分为以下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的懒汉式双端检锁机制代码,在上面代码的基础上将实例声名加上volatile:

private static volatile SingletonDemo singletonDemo;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值