并发编程-volatile使用

  • Post author:
  • Post category:其他




第一章 volatile关键字概览



1.1 多线程下变量的不可见性



1.1.1 概述

在多线程并发执行下,多个线程修改共享的成员变量,会出现一个线程修改了共享变量的值后,另一个线程不能直接看到该线程修改后的变量的最新值



1.1.2 案例演示

/*
    目标:研究一下多线程下变量访问的不可见性现象

    准备内容:
        1.准备2个线程
        2.定义一个成员变量
        3.开启两个线程,其中一个线程负责修改,一个线程负责读取
 */
public class VisibilityDemo01 {
    // main方法,作为一个主线程
    public static void main(String[] args) throws InterruptedException {
        myThread myt = new myThread();
        Thread t = new Thread(myt);
        t.start();

        while(true){
            if(myt.isFlag()) {
                System.out.println("主线程");
            }
        }
    }
}

class myThread implements Runnable{
    //成员变量
    private boolean flag = false;

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;
        System.out.println(true);
    }
}

19.多线程中卖电影票、送牛奶均为案例演示



1.1.5 结果展示

在这里插入图片描述



1.1.4 小结

多线程下修改共享变量会出现变量修改值后的不可见性



1.2 变量不可见性内存语义



1.2.1 概述



1.3 变量不可见性解决方案



1.3.2 结局方案


加锁


某一个线程进入synchronized代码块前后,执行过程如下:

a.线程获得锁

b.清空工作内存

c.从主内存拷贝共享变量最新的值到工作内存成为副本

d.执行代码

e.将修改后的副本的值刷新回主内存中

f.线程释放锁


volatile关键字修饰


a.子线程从主内存读取到数据放入其对应的工作内存

b.将flag的值更改为true,但是这个时候flag的值还没有写回主内存

c.此时main方法读取到了flag的值为flase

d.当子线程将flag的值写回去后,其他所有线程对此变量的副本都失效

e.再次对flag进行操作的时候,操作的线程就会从主内存读取最新的值,放入到工作内存中


总结

volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,当修改写回主内存时,另外一个线程立即看到最新的值



第二章 volatile的其他特性



2.1 volatile特性概述


volatile总体概览


在上一章中,我们已经研究完了volatile可以实现并发下共享变量的可见性,除了volatile可以保证可见性外,volatile还具备如下一些突出的特性:


volatile的原子性问题

:volatile不能保证原子性操作。


禁止指令重排序

:volatile可以防止指令重排序操作。



2.2 volatile不保证原子性

所谓原子性是指在一次操作或者多次操作中,要么所有的操作全部都得到了执行并且不会受到任何因素的干扰而中断,要么所有的操作都不执行。volatile不保证原子性



2.2.1代码测试:

public class VolatileDemo {
    public static void main(String[] args) {
        VolatileAtomicThread vat = new VolatileAtomicThread();

        for(int x=0;x<100;x++){
            new Thread(vat).start();
        }
        System.out.println(vat.getI());
    }
}

class VolatileAtomicThread implements Runnable{
    private volatile int i=0;

    public int getI() {
        return i;
    }

    @Override
    public void run() {
        for(int j=0;j<10000;j++){
            i++;
            System.out.println("count" + i);
        }
    }
}



2.2.2 小结

在多线程环境下,volatile关键字可以保证共享数据的可见性,但是并不能保证对数据操作的原子性(在多线程环境下volatile修饰的变量也是线程不安全的)。

在多线程环境下,要保证数据的安全性,我们还需要使用锁机制

经自己尝试,synchronized、lock都能保证原子性



2.2.3 问题解决


使用锁机制


synchronized、lock


原子类


概述:java从JDK1.5开始提供了java.util.concurrent。atomic包(简称Atomic包),这个包中的原子操作类提供一种用法简单,性能高效,线程安全地更新一个变量地方式。


AromicInteger


原子型Integer,可以实现原子更新操作

class VolatileAtomicThread01 implements Runnable{
    private AtomicInteger ai = new AtomicInteger(); //初始值默认为0
    @Override
    public void run() {
        for(int j=0;j<10000;j++){
            System.out.println(Thread.currentThread().getName() + "count" + ai.incrementAndGet());
        }
    }
}



2.3 禁止指令重排序



2.3.1 概述


什么是重排序

:为了提高性能,编译器和处理器常常会对既定地代码执行顺序进行指令重排序

原因:一个好的内存模型实际上会放松对处理器和编译器规则的舒服,也就是说软件技术和硬件技术都为同一个目标而进行奋斗:在不改变程序执行结果的前提下,尽可能提高执行效率。JMM对底层尽量减少约束,使其能够发挥自身优势。因此,在执行程序时,

为了提高性能,编译器和处理器常常会对指令进行重排序

。一般重排序可以分为如下三种:

1.编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;

2.指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果

不存在数据依赖性

,处理器可以改变语句对应机器指令的执行顺序;

3.内存系统的重排序。由于处理器使用缓存和读/写缓存区,这使得加载和存储操作看上去可能是在乱序执行的。

在这里插入图片描述



2.3.2 重排序的好处


重排序可以提高处理的速度


在这里插入图片描述



2.3.3 重排序问题案例演示

重排序虽然可以提高执行的效率,但是在并发执行下,JVM虚拟机底层并不能保证重排序下带来的安全性等问题,请看如下案例:

public class OutOfOrderDemo {
    public static int a = 0, b = 0;
    public static int i = 0, j = 0;
    public static void main(String[] args) throws Exception {
        //定义两个线程
        //线程A
        while(true) {
            a=0;b=0;i=0;j=0;
            Thread t1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 1;
                    i = b;
                }
            });
            //线程B
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    j = a;
                }
            });
            t1.start();
            t2.start();
            t1.join(); //让t1线程优先执行完毕
            t2.join(); //让t2线程优先执行完毕
            // 得到线程执行完毕后的结果
            System.out.println("i=" + i + ",j=" + j);
            if(i==0&&j==0){
                break;
            }
        }
    }
}

观察两个线程中的代码

线程1:a=1;

i=b;

线程2:b=1;

j=a;

我们预期的输出结果只可能有三种
在这里插入图片描述


现象分析

发生了重排序:在线程1和线程2内部的两行代码的实际执行顺序和代码在Java文件中的顺序是不一致的,代码指令并不是严格按照代码顺序执行的,他们的顺序改变了,这样就是发生了重排序,这里颠倒的是 a=1,i=b 以及 j=a,b=1 的顺序,从而发生了指令重排序。直接获取了 i=b(0),j=a(0)的值。



2.3.4 volatile禁止重排序

volatile修饰变量后可以实现禁止指令重排序



2.3.5 小结

使用volatile可以禁止指令重排序,从而修正重排序可能带来的并发安全问题



第三章 volatile内存语义



3.1 volatile写读建立的happens-before关系



3.1.1 概述

上面的内容讲述了重排序原则,为了提高处理速度,JVM会对代码进行编译优化,也就是指令重排序优化,并发编程下指令重排序会带来一些安全隐患:如指令重排序导致的多个线程操作之间的不可见性。如果让程序员再去了解这些底层的实现以及具体规则,那么程序员的负担就太重了,严重影响了并发编程的效率。

从JDK 5开始,提出了happens-before的概念,通过这个概念来阐述操作之间的内存可见性。如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

所以为了解决多线程的可见性问题,就搞出了happens-before原则,让线程之间遵守这些原则。编译器还会优化我们的语句,所以等于是给了编译器优化的约束。不能让它优化的不知道东南西北了!

简单来说: happens-before应该翻译成:前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。



3.1.2 happens-before规则

具体的一共有六项规则:

1.

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


●解释:一个线程中的每个操作,happens-before于该线程中的任意后续操作

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

2.

锁规则(Synchronized,Lock等)


● 解释:对一个锁的解锁,happens-before于随后对这个锁的加锁

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

3.

volatile变量规则


● 解释:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。



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

4.

传递性


● 解释:如果A happens-before B,且B happens-before C,那么A happens-before C.

○ A h-b B,B h-b C那么可以得到Ah-b C

5.

start()规则


● 解释:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。

○ 假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行前对线程B可见。注意:线程B启动之后,线程A在对变量修改线程B未必可见

6.

join()规则


● 解释:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.jion()操作成功返回。

○ 线程t1写入的所有变量,在任意其他线程t2调用t1.join(),或者t1.isAlive()成功返回后,都对t2可见



3.1.3 volatile写读建立的happens-before规则

public class VisibilityHPDemo {
    private static int a = 1;
    private volatile static int b = 2;

    public static void write() {
        a = 3;
        b = a;
    }

    public static void read() {
        System.out.println("a=" + a + "   b=" + b);
    }

    public static void main(String[] args) {
        a = 1;
        b = 2;
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                write();
            }
        });
        t1.start();
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                read();
            }
        });
        t2.start();
    }
}

分析以上案例存在四种执行情况:

b=3;a=3

b=2;a=1

b=2;a=3

第四种情况(低概率):没给b加volatile,那么有可能出现a=1,b=3.因为a虽然被修改了,但是其他线程不可见,而b恰好其他线程可见,造成了b=3,a=1.

如何解决第四种情况呢?

按照happens-before规则,我们只需要给b加上volatile,那么b之前的写入(a=3;)将对读取b之后的代码可见,也就是说即使a不加volatile,只要 b读取到3,那么b之前的操作就一定是可见的,此时就不会出现第四种情况了。



3.1.4 volatile重排序规则小结

在这里插入图片描述

● 写volatile变量时,无论前一个操作是什么,都不能重排序

● 读volatile变量是,无论后一个操作时什么,都不能重排序

● 当先写volatile变量,后读volatile变量时,不能重排序



4.第四章 volatile高频面试与总结



4.1 long和double的原子性

概述

在java中,long和double都是8个字节共64位(一个字节=8bit),那么如果是一个32位的系统,读写long或double的变量时会涉及到原子性问题,因为32位的系统要读完一个64位的变量,需要分两步执行,每次读取32位,这样就对double和long变量的赋值就会出现问题:

如果有两个线程同时写一个变量内存,一个进程写 低32位,而另一个写 高32位,这样将导致获取的64位数据是失效的数据

案例演示:

public class LongAndDouble implements Runnable{
    private long value;
    private static long aLong = 0;

    public LongAndDouble(long value) {
        this.value = value;
    }

    public long getValue() {
        return value;
    }

    public void setValue(long value) {
        this.value = value;
    }

    @Override
    public void run() {
        int i=0;
        while(i<10000){
            aLong = this.getValue();
            i++;

            long temp = aLong;

            if(temp!= 1L && temp !=-1L){
                System.out.println("出现错误结果" + temp);
                System.exit(0);
            }
            i++;
        }
        System.out.println("运行正确");
    }

    public static void main(String[] args) {
        LongAndDouble lad1 = new LongAndDouble(1);
        LongAndDouble lad2 = new LongAndDouble(-1);

        Thread t1 = new Thread(lad1);
        Thread t2 = new Thread(lad2);

        t1.start();
        t2.start();
    }
}


测试结果


上面的代码在32位环境下和64位环境执行的结果是不一样的:

32位环境:

出现错误结果

原因:32位环境无法一次读取long类型数据,多线程环境下对Long变量的读写是不完整的,导致temp变量既不等于1也不等于-1。出现了long和double读写的原子性问题了。

64位环境:运行正确


小结


结论:如果是在64位的系统中,那么对64位的long和double的读写都是原子操作的。即可以一次性读写long或double的整个64bit。如果在32位的JVM上,long和double就不是原子性操作了。


解决方法

: 需要使用volatile关键字来防止此类现象

● 对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作。

● 如果使用volatile修饰long和double, 那么其读写都是原子操作;

● 在实现JVM时,可以自由选择是否把读写long和double作为原子操作;

● java中对于long和double类型的写操作不是原子操作,而是分成了两个32位的写操作。读操作是否也分成了两个32位的读呢?在JSR-133之 前的规范中,读也是分成了两个32位的读,但是从JSR-133规范开始,即JDK5开始,读操作也都具有原子性;

● java中对于其他类型的读写操作都是原子操作(除long和double类型以外);

● 对于引用类型的读写操作都是原子操作,无论引用类型的实际类型是32位的值还是64位的值;

● Java商业虚拟机已经解决了long和double的读写操作的原子性。



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



4.2.1 单例概述

单例是需要在内存中永远只能创建一个类的实例,

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

单例模式的使用场景:

● 全局信息类:例如任务管理器对象,或者需要一个对象记录整个网站的在线流量等信息

● 无状态工具类:类似于整个系统的日志对象等,我们只需要一个单例日志对象负责记录,管理系统日志信息。



4.2.2单例模式有8种

单例模式我们可以提供出8种写法,有很多我们存在

饿汉式单例的概念

,以及

懒汉式单例的概念


饿汉式单例的含义是

:在获取单例对象之前对象已经创建完成了。


懒汉式单例是指

:在真正需要单例的时候才创建出该对象。(在java程序中,有时会需要推迟一些高开销的对象初始化工作,只有在需要这个对象的时候才进行初始化,所以程序员需要对这个对象延迟初始化)



饿汉单例的2种写法


特点

:在获取单例对象之前对象已经创建完成了。



① 饿汉式(静态常量)
/*
    目标:饿汉式(静态常量)
    步骤:
        1.构造器私有。 (必须把构造器设为私有"藏起来",不然外部就可以创建多个单例对象,私有后对外就不能创建对象了。
        2.定义一个静态常量保存一个唯一的实例对象(单例)
        3.提供一个方法,返回单例对象
 */
public class Singleton01 {
    //2.定义一个静态常量保存一个唯一的实例对象(单例)
    public static final Singleton01 INSTANCE = new Singleton01();
    //1.构造器私有
    private Singleton01(){
    }
    //3.提供一个方法返回单例对象(对外暴露该单例)
    public static Singleton01 getInstance(){
        return INSTANCE;
    }
}
class Test01{
    public static void main(String[] args) {
        Singleton01 s1 = Singleton01.getInstance();
        Singleton01 s2 = Singleton01.getInstance();
        System.out.println(s1 == s2);
    }
}


② 饿汉式(静态代码块)
/*
    目标:饿汉式(静态常量)
    步骤:
        1.构造器私有。 (必须把构造器设为私有"藏起来",不然外部就可以创建多个单例对象,私有后对外就不能创建对象了。
        2.定义一个静态常量保存一个唯一的实例对象(单例),可以通过静态代码块初始化单例对象。
        3.提供一个方法,返回单例对象
 */
public class Singleton02 {
    //2.定义一个静态常量保存一个唯一的实例对象(单例)
    public static final Singleton02 INSTANCE;
    static{
        INSTANCE = new Singleton02();
    }
    //1.构造器私有
    private Singleton02(){
    }
    //3.提供一个方法返回单例对象(对外暴露该单例)
    public static Singleton02 getInstance(){
        return INSTANCE;
    }
}
class Test02{
    public static void main(String[] args) {
        Singleton02 s1 = Singleton02.getInstance();
        Singleton02 s2 = Singleton02.getInstance();
        System.out.println(s1 == s2);
    }
}



懒汉单例的4种写法


特点

:在真正需要单例的时候才创建出该对象。在java程序中,有时候可能需要推迟一些高开销对象的初始化操作,并且只有在使用这些对象的时候才初始化,此时,程序员可能会采用延迟初始化。


值得注意的是:要正确的实现线程安全的延迟初始化还是需要一些技巧的,否则很容易出现问题



③ 懒汉式(线程不安全)

不推荐

/*
    目标:懒汉式(线程不安全的写法)。
    步骤:
        1.构造器私有。
        2.定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
        3.定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,有则直接返回,没有就创建一个新的单例对象。
 */
public class Singleton03 {
    //2.定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
    private static Singleton03 INSTANCE;
    //1.构造器私有
    private Singleton03() {}
    //3.定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,有则直接返回,没有就创建一个新的单例对象。
    public static Singleton03 getInstance(){
        if(INSTANCE == null){    //线程不安全之处就在这里,假如两个线程同时执行到这一步,都判断为空,就创建了两次对象。
            //说明这是第一次来拿单例对象,需要真正的创建出来;
            INSTANCE = new Singleton03();
        }
        return INSTANCE;
    }
}


④ 懒汉式(线程安全)

性能差,不推荐

分析

使用Synchronized关键字修饰方法包装线程安全,但性能较差,并发下只能有一个线程正在进入获取单例对象。

/*
    目标:懒汉式(线程安全的写法)。
    步骤:
        1.构造器私有。
        2.定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
        3.定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,有则直接返回,没有就创建一个新的单例对象。
        4.为获取单例的方法加锁:用synchronized
 */
public class Singleton04 {
    //2.定义一个静态的变量存储一个单例对象(定义的时候不初始化该对象)
    private static Singleton04 INSTANCE;
    //1.构造器私有
    private Singleton04() {}
    //3.定义一个获取单例的方法,每次返回单例对象的时候先询问是否有对象,有则直接返回,没有就创建一个新的单例对象。
    public synchronized static Singleton04 getInstance(){
        if(INSTANCE == null){    //线程不安全之处就在这里,假如两个线程同时执行到这一步,都判断为空,就创建了两次对象。
            //说明这是第一次来拿单例对象,需要真正的创建出来;
            INSTANCE = new Singleton04();
        }
        return INSTANCE;
    }
}


⑤ 懒汉式(线程不安全)

线程不安全,不推荐

**特点:**是一种优化后的似乎线程安全的机制

/*
    目标:懒汉式(线程不安全)
    步骤:
        1.构造器私有。
        2.定义一个静态变量存储一个单例对象。
        3.提供一个方法返回一个单例对象。
 */
public class Singleton05 {
    //2.定义一个静态变量存储一个单例对象
    private static Singleton05 INSTANCE;
    //1.构造器私有
    private Singleton05(){
    }
    //3.返回一个单例对象
    public static Singleton05 getInstance(){
        //判断单例对象的变量是否为null;
        if(INSTANCE == null){
            // 很多个线程执行到这里
            synchronized (Singleton05.class){
                INSTANCE = new Singleton05();
            }
        }
        return INSTANCE;
    }
}




懒汉式(volatile双重检查模式,推荐)

/*
    目标:双重检查机制,以及使用volatile修饰(最好,最安全的方式,推荐写法)

    步骤:
        1.构造器私有
        2.提供了一个静态变量用于存储单例对象。
        3.提供一个方法进行双重检查机制返回单例对象。
        4.必须使用volatile修饰静态变量。   ?
 */
public class Singleton06 {

    //3.提供一个方法进行双重检查机制返回单例对象
    public static Singleton06 getInstance() {
        //第一重检查
        if (INSTANCE == null) {
            //同步锁定代码块
            synchronized (Singleton06.class) {
                //第二重检查
                if (INSTANCE == null) {
                    //注意:非原子操作
                    INSTANCE = new Singleton06();
                }
            }
        }
        return INSTANCE;
    }

    //2.提供一个静态变量用于存储单例对象。
    private volatile static Singleton06 INSTANCE;

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


分析


双重检查的优点:线程安全,延迟加载,效率较高!


思考


为什么还要加上volatile保证安全

在这里插入图片描述

1、禁止指令重排序

● 所以,new Singleton()是一 个非原子操作,编译器可能会重排序[构造函数可能在整个对象初始化完成前执行完毕,即赋值操作(只是在内存中开辟-片存储区域后直接返回内存的引用) 在初始化对象前完成]。而线程B在线程A赋值完时判断instance就不为null了,此时B拿到的将是一个没有初始化完成的半成品。这样是很危险的。因为极有可能线程B会继续拿着个没有初始化的对象中的数据进行操作,此时容易触发”NPE异常”

2、保证可见性

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

● 如果加上了volatile修饰instance之后,保证了可见性,一旦线程A返回了实例,线程B可以立即发现instance不为null。



⑦ 静态内部类单例方式

引入:JVM在类初始化阶段(即在Class被加载后,且线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

基于这个特性,可以实现另一种线程安全的延迟初始化方案

/*
    目标:基于类的初始化实现延迟加载和线程安全的单例设计。

    步骤:
        1.构造器私有。
        2.提供一个静态内部类,里面提供一个常量存储一个单例对象
        3.提供一个方法返回静态内部类中的单例对象
 */
public class Singleton07 {

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

    //2.提供一个静态内部类,里面提供一个常量储存一个单例对象
    private static class Inner{
        private static final Singleton07 INSTANCE = new Singleton07();
    }

    //3.提供一个方法返回静态内部类中的单例对象
    public static Singleton07 getInstance(){
        return Inner.INSTANCE;
    }
}


小结


1.静态内部类是在被调用时才会被加载,这种方案实现了懒汉单例的一种思想,需要用到的时候才去创建单例,加上JVM的特性,这种方式又实现了线程安全的创建单例对象。

2.通过对比基于volatile的双重检查锁定方案和基于类初始化方案的对比,我们会发现基于类初始化的方案的实现代码更加简洁。但是基于volatile的双重检查锁定方案有一个额外的优势:

除了可以对静态字段实现延迟加载初始化外,还可以对实例字段实现延迟初始化



⑧ 枚举实现单例

/*
    目标:枚举实现单例。

    引入:枚举实际上是一种多例的模式。如果我们直接定义一个实例就相当于是单例了。
 */
public enum Singleton08 {
    INSTANCE;
}



4.3 volatile的使用场景



4.3.1 纯赋值操作



概述

volatile 不适合做a++等操作。

适合做纯赋值操作:如boolean flag = flase/true;

/*
    目标:volatile的使用场景介绍。
        1.适合做纯赋值操作,不适合做a++等带运算的操作。

 */
public class UserVolatile implements Runnable{
    volatile boolean flag = false;
    AtomicInteger realA = new AtomicInteger();

    @Override
    public void run() {
        for(int i=1;i<=10000;i++){
            switchFlag();
            realA.incrementAndGet();
        }
    }

    public void switchFlag(){
//        flag = true; //纯赋值操作
        flag = !flag; //这样做不符合预期
    }
}

class Test{
    public static void main(String[] args) throws InterruptedException {
        UserVolatile uv = new UserVolatile();
        Thread t1 = new Thread(uv);
        Thread t2 = new Thread(uv);

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(uv.flag);
        System.out.println(uv.realA);
    }
}


小结


volatile可以适合做多线程中的纯赋值操作:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是由原子性的,而volatile又保证了可见性,所以就足以保证线程安全



4.3.2 触发器



概念

按照volatile的可见性和禁止重排序以及happens-before规则,volatile可以作为刷新之前变量的触发器。我们可以将某个变量设置为volatile修饰,其他线程一旦发现该变量修改的值后,触发获取到该变量之前的操作都将是最新的且可见。



案例演示

public class UserVolatile02 {
    int a = 1;
    int b = 2;
    int c =3 ;
    volatile boolean flag = false;

    public void write(){
        a = 100;
        b=200;
        c = 300;

        flag = true;  // 修饰volatile的变量
    }
    public void read(){
        while(flag){
            System.out.println(a + "," + b + "," + c);
        }
    }

    public static void main(String[] args) {
        UserVolatile02 uv2 = new UserVolatile02();

        new Thread(new Runnable() {
            @Override
            public void run() {
                uv2.write();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                uv2.read();
            }
        }).start();
    }
}



小结

volatile可以作为刷新之前变量的触发器。我们可以将某个变量设置为volatile修饰,其他线程一旦发现该变量修改的值后,触发获取到的该变量之前的操作都将是最新的且可见的。



4.4 volatile和synchronized



4.4.1 区别

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

● volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。

● volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题。

● volatile可以看作是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值,而没有其他操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。



4.5 volatile的总结



4.5.1 总体总结

1.volatile修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器,实现轻量级同步。

2.volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。

3.volatile只能作用于属性,我们用volatile修饰属性,这样compiler就不会对这个属性做指令重排序。

4.volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile属性不会被线程缓存,始终从主存中读取。

5.volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作。

6.volatile可以使得long和double的赋值是原子的。(即使是在32位系统中对long和double的赋值)

7.volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。



版权声明:本文为Twilight___原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。