volatile的详细解释以及应用场景

volatile关键字概述

概述

多个线程修改共享的成员变量,会出现一个线程修改了成员变量,另一个线程不能立即看到该线程修改变量后的最新值

问题

所有共享变量存在于主内存,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。

解决

第一种是加锁,第二种是volatile关键字

volatile的特性

概述

1.volatile不能保证原子性问题

2.volatile可以防止指令重排序操作

volatile不能保证原子性问题

首先需要了解的是java对基本类型变量的读取和赋值是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作就不是原子类操作,他可以拆分为多次原子操作,比如先读取i,再将i的值赋值为j。所以,一个变量被volatile修饰了,那么可以保证这个变量在读取的时候值是最新的,但是一旦对变量进行自增这样的非原子操作,就不能保证。

比如一个变量i被volatile修饰,两个线程都对这个变量进行修改,都对其进行i++操作,i++可以分为三步:先获取到变量i的值,其次对i的值+1,最后将新值写到缓存中。线程A首先得到变量i的值100,还没来得及修改就阻塞了,这时线程B也得到了i,并且做完了i++,由于volatile主内存这时候的值是101。但是线程A已经读到了i的值100,也就是说这个原子操作已经结束了,所以这个可见性来的有点晚,所以线程A做完操作后刷新到主内存,i的值在主内存还是101

禁止指令重排序

重排序的好处:

提高程序运行的速度

重排序带来的安全性问题:

每个线程内部的代码不一定按代码顺序执行,由于指令重排序可能顺序颠倒造成安全性问题,也就是多个线程之间操作的不可见性。

volatile禁止重排序

变量被volatile修饰后,不能发生重排序

volatile的内存语义

概述

happends-before表示前一个操作的结果可以被后续的操作获取,比如前面一个操作将变量a赋值成1,后面一个操作一定可以知道a变成1了。

happends-before规则

程序顺序规则(单线程规则)

一个线程的每个操作,happends-before于该线程中的任意操作。

同一个线程中前面所有的写操作对后面的操作可见。

锁规则

对一个锁的解锁,happends-before于后续对这个锁的加锁。

如果线程1解锁了monitor a,接着线程2锁定了a,那么线程1解锁a之前的操作对线程2可见(线程1和线程2可以是同一个线程)

volatile变量规则

对一个volatile域的写,happends-before于后续对这个volatile域的读

如果线程1写入了volatile变量v(临界资源),线程2读取了v,那么线程1写入v之前的写操作都对线程2可见(线程1和线程2是同一个操作)

传递性

如果A happends-before B, B happends-before C, 那么A happends-before C

start()规则

主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

注意:线程B启动之后,线程A对变量修改线程B未必可见

 join()规则

线程A执行ThreadB.join() 成功返回,那么线程B中的任意操作happends-before于线程A从ThreadB.join()操作成功返回

详细解释:线程t1写入的所有变量,在任意其他线程t2调用t1.join(),或者t1.Alive()返回后,都对t2可见

volatile写读建立的happends-before规则的演示

 小结

volatile面试与总结

long和double的原子性

在java中,long和double都是64位,分为高32位和低32位,如果是一个32位的系统,读和写long和double涉及到原子性问题,因为32位的系统要读完一个64位的变量,要分为两步,每次读取32位。

那么如果有两个线程写一个变量内存,一个进程写低32位,一个进程写高32位,就会造成该64位数据是失效的数据

不过是64位的系统上对long和double的操作都是一次性读写的

32位解决方法:

变量用volatile修饰

volatile在双重检查加锁的单例中的作用

单例概述

单例是需要在内存中永远只有一个类的实例

单例的作用:节约内存和保证共享计算的结果正确,以及方便管理

单例模式有8种

存在饿汉单例模式和懒汉单例模式

饿汉单例:在获取单例对象之前已经创建了

懒汉单例:在真正需要单例的时候才创建出对象

饿汉单例模式的两种写法

饿汉式(静态常量)
public class Singleton1 {
    //2.定义一个静态常量保存一个唯一的实例对象【单例】
    private static final Singleton1 INSTANCE = new Singleton1();

    //1.构造器私有
    private Singleton1(){
    }

    //3.提供一个方法返回单例对象
    public static Singleton1 getInstance(){
        return INSTANCE;
    }
}
饿汉式(静态代码块)
public class Singleton2{
    //2.定义一个静态常量保存一个唯一的实例对象【单例】
    private static final Singleton2 INSTANCE;
    //通过静态代码块创建的对象
    static {
        INSTANCE = new Singleton2();
    }
    //1.构造器私有
    private Singleton2(){

    }

    //3.提供一个方法返回单例对象
    public static Singleton2 getInstance(){
        return INSTANCE;
    }
}

懒汉单例的4种写法

特点:在真正需要单例对象的时候才创建出对象。在java程序中,有时候可能需要推迟一些高开销对象的初始化操作,并且只有在用到这些对象的时候才初始化,也就是延迟初始化。

懒汉式(线程不安全)
public class Singleton3 {
    //2.声明一个静态变量,用于存放对象
    private static Singleton3 INSTANCE;


    //1.私有构造器
    private Singleton3(){

    }

   //3.提供一个方法用于获取对象:先询问对象是否存在,若已经存在对象,则返回。如果对象为null,则创建对象
    public static Singleton3 getINSTANCE(){
        if(INSTANCE == null){
            //进入此:说明这是第一次创建对象
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }

}

线程不安全:如果存在同时存在两个或多个线程同时进入INSTANCE == null这一步,就违背了单例原则。

懒汉式(线程安全)
public class Singleton4 {
    //2.声明一个静态变量,用于存放对象
    private static Singleton4 INSTANCE;

    //1.私有构造器
    private Singleton4(){
    }

   //3.提供一个方法用于获取对象:先询问对象是否存在,若已经存在对象,则返回。如果对象为null,则创建对象
   //4.对对象的获取方法使用synchronized修饰
    public synchronized static Singleton4 getINSTANCE(){
        if(INSTANCE == null){
            INSTANCE = new Singleton4();
        }
        return INSTANCE;
    }

}

性能较差,并发下只能有一个线程获取单例对象

懒汉式(线程不安全,优化后的似乎线程安全)
public class Singleton5 {
    //2.声明一个静态变量,用于存放对象
    private static Singleton5 INSTANCE;

    //1.私有构造器
    private Singleton5(){
    }

   //3.提供一个方法用于获取对象:先询问对象是否存在,若已经存在对象,则返回。如果对象为null,则创建对象
    public static Singleton5 getINSTANCE(){
        if(INSTANCE == null){
            //存在问题:当第一个线程创建对象后,第二个线程会再次创建对象
            synchronized (Singleton5.class){
                INSTANCE = new Singleton5();
            }
        }
        return INSTANCE;
    }

}
懒汉式(双重检查 线程安全)

最好,最安全的方式,推荐写

public class Singleton6 {
    //2.提供静态变量保存实例对象
    private volatile static Singleton6 INSTANCE;

    //1.私有化构造器
    private Singleton6(){}

    //3.提供获取对象的方法
    public static  Singleton6 getInstance(){
        //第一重检查:针对很多个线程同时想要创建对象的情况
        if(INSTANCE == null){
            //同步代码块锁定
            synchronized (Singleton6.class){
     //第二重锁检查(针对比如A,B两个线程都为null,第一个线程创建完对象,第二个等待锁的线程拿到锁的情况)
                if(INSTANCE == null){
                    INSTANCE = new Singleton6();
                }
            }
        }
        return INSTANCE;
    }
}
双重检查的懒汉模式为什么静态成员变量还要加上volatile修饰?

1.禁止指令重排序

new Singleton6()不是一个原子操作,编译器可能会指令重排序,使得分配内存后先返回引用再构造器初始化对象。而线程B在线程A返回引用后判断INSTANCE就不为null,拿到了将是一个未初始化对象的半成品,容易触发“NPE异常”,也就是空指针异常。

2.保证可见性

由于可见性问题,线程A在自己的工作线程创建了实例,但还未同步到主内存中,此时线程B在工作内存中判断INSTANCE还是null,那么线程B又创建了一个实例。

加了volatile以后保证及时同步到主内存,线程B可以立即发现INSTANCE不为null

静态类部类单例方法

public class Singleton7 {
    //1.私有化构造器
    private Singleton7(){

    }
    //2.提供一个静态内部类创建实例对象
    private static class Inner{
        private static final Singleton7 INSTANCE = new Singleton7();
    }

    //3.提供一个方法用于获取实例对象
    public static Singleton7 getInstance(){
        return Inner.INSTANCE;
    }
}

优点:

1.静态类部类保证在调用时才被加载,符合懒汉设计模式。加上在获取类的期间,jvm会获取一个锁,保证了线程安全

2.对比双重检查,代码更简洁。

但双重检查除了对静态字段实现延迟加载初始化外,还可以对实例字段实现延迟初始化。

枚举实现单例

public enum  Singleton8 {
    //实际上枚举是一种多例的模式,但可以直接定义一个实例就相当于单例了
    INSTANCE;
}

volatile应用场景

适合做纯赋值操作,不适合做a++操作

图中,flag被volatile修饰,初始值是false

触发器

volatile作为刷新之前变量的触发器,一旦某个线程读取到volatile修饰的变量,那么触发获取到该之前的变量都是最新且可见的。因为volatile保证可见性和禁止指令重排序。

volatile与synchronized的区别

1.volatile只能修饰实例变量和类变量,synchronized可以修饰方法以及代码块

2.volatile可以保证数据的可见性,但是不能保证原子性,而synchronized是一种互斥的机制,可以保证可见性和原子性

3.volatile用于禁止指令重排序,可以解决单双例检查对象初始化代码执行乱序的情况

4.volatile是线程同步的轻量级实现,性能比synchronized好。

5.volatile不会造成线程的阻塞,synchronized可能会造成线程的阻塞

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值