谈一谈 volatile |8月更文挑战

143 阅读7分钟

image.png

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

volatile是java虚拟机提供的轻量级的同步机制

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

1.保证可见性

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

当不添加volatile关键字时示例:

package com.jian8.juc;

import java.util.concurrent.TimeUnit;

/**
 * 1验证volatile的可见性
 * 1.1 如果int num = 0,number变量没有添加volatile关键字修饰
 * 1.2 添加了volatile,可以解决可见性
 */
public class VolatileDemo {

    public static void main(String[] args) {
        visibilityByVolatile();//验证volatile的可见性
    }

    /**
     * volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改
     */
    public static void visibilityByVolatile() {
        MyData myData = new MyData();

        //第一个线程
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\t come in");
            try {
                //线程暂停3s
                TimeUnit.SECONDS.sleep(3);
                myData.addToSixty();
                System.out.println(Thread.currentThread().getName() + "\t update value:" + myData.num);
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }, "thread1").start();
        //第二个线程是main线程
        while (myData.num == 0) {
            //如果myData的num一直为零,main线程一直在这里循环
        }
        System.out.println(Thread.currentThread().getName() + "\t mission is over, num value is " + myData.num);
    }
}

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

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

输出结果:

thread1 come in

thread1 update value:60

//线程进入死循环

当我们加上volatile关键字后,volatile int num = 0;

输出结果为:

thread1 come in

thread1 update value:60

main mission is over, num value is 60 //程序没有死循环,结束执行。

2. 不保证原子性

原子性:不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败

验证示例(变量添加volatile关键字,方法不添加synchronized):

package com.jian8.juc;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 1验证volatile的可见性
 *  1.1 如果int num = 0,number变量没有添加volatile关键字修饰
 * 1.2 添加了volatile,可以解决可见性
 *
 * 2.验证volatile不保证原子性
 *  2.1 原子性指的是什么
 *      不可分割、完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割,需要整体完整,要么同时成功,要么同时失败
 */
public class VolatileDemo {

    public static void main(String[] args) {
//        visibilityByVolatile();//验证volatile的可见性
        atomicByVolatile();//验证volatile不保证原子性
    }
    
    /**
     * volatile可以保证可见性,及时通知其他线程,主物理内存的值已经被修改
     */
	//public static void visibilityByVolatile(){}
    
    /**
     * volatile不保证原子性
     * 以及使用Atomic保证原子性
     */
    public static void atomicByVolatile(){
        MyData myData = new MyData();
        for(int i = 1; i <= 20; i++){
            new Thread(() ->{
                for(int j = 1; j <= 1000; j++){
                    myData.addSelf();
                    myData.atomicAddSelf();
                }
            },"Thread "+i).start();
        }
        //等待上面的线程都计算完成后,再用main线程取得最终结果值
        try {
            TimeUnit.SECONDS.sleep(4);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+"\t finally num value is "+myData.num);
        System.out.println(Thread.currentThread().getName()+"\t finally atomicnum value is "+myData.atomicInteger);
    }
}

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

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

    public void addSelf(){
        num++;
    }
    
    AtomicInteger atomicInteger = new AtomicInteger();
    public void atomicAddSelf(){
        atomicInteger.getAndIncrement();
    }
}

执行三次结果为:

//1.

main finally num value is 19580

main finally atomicnum value is 20000

//2.

main finally num value is 19999

main finally atomicnum value is 20000

//3.

main finally num value is 18375

main finally atomicnum value is 20000 //num并没有达到20000

3.禁止指令重排

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

graph LR
	源代码 --> id1["编译器优化的重排"]
	id1 --> id2[指令并行的重排]
	id2 --> id3[内存系统的重排]
	id3 --> 最终执行的指令
	style id1 fill:#ff8000;
	style id2 fill:#fab400;
	style id3 fill:#ffd557;

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

重排代码实例:

声明变量:int a,b,x,y=0

线程1	线程2
x = a;	y = b;
b = 1;	a = 2;
结 果	x = 0 y=0
如果编译器对这段程序代码执行重排优化后,可能出现如下情况:

线程1	线程2
b = 1;	a = 2;
x= a;	y = b;
结 果	x = 2 y=1
这个结果说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的

volatile实现禁止指令重排,从而避免了多线程环境下程序出现乱序执行的现象

==内存屏障==(Memory Barrier)又称内存栅栏,是一个CPU指令,他的作用有两个:

  • 保证特定操作的执行顺序。
  • 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。 由于编译器和处理器都能执行指令重排优化。如果在之零件插入一i奥Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排顺序,也就是说==通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化==。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
graph TB
    subgraph 
    bbbb["对Volatile变量进行读操作时,<br>回在读操作之前加入一条load屏障指令,<br>从内存中读取共享变量"]
    ids6[Volatile]-->red3[LoadLoad屏障]
    red3-->id7["禁止下边所有普通读操作<br>和上面的volatile读重排序"]
    red3-->red4[LoadStore屏障]
    red4-->id9["禁止下边所有普通写操作<br>和上面的volatile读重排序"]
    red4-->id8[普通读]
    id8-->普通写
    end
    subgraph 
    aaaa["对Volatile变量进行写操作时,<br>回在写操作后加入一条store屏障指令,<br>将工作内存中的共享变量值刷新回到主内存"]
    id1[普通读]-->id2[普通写]
    id2-->red1[StoreStore屏障]
    red1-->id3["禁止上面的普通写和<br>下面的volatile写重排序"]
    red1-->id4["Volatile写"]
    id4-->red2[StoreLoad屏障]
    red2-->id5["防止上面的volatile写和<br>下面可能有的volatile读写重排序"]
    end
    style red1 fill:#ff0000;
    style red2 fill:#ff0000;
    style red4 fill:#ff0000;
    style red3 fill:#ff0000;
    style aaaa fill:#ffff00;
    style bbbb fill:#ffff00;

你在那些地方用过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(getInstance() == getInstance());
//        System.out.println(getInstance() == getInstance());
//        System.out.println(getInstance() == getInstance());

        //并发多线程后,构造方法会在一些情况下执行多次
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, "Thread " + i).start();
        }
    }
}

其构造方法在一些情况下会被执行多次

解决方式: 单例模式DCL代码 DCL (Double Check Lock双端检锁机制)在加锁前和加锁后都进行一次判断

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

大部分运行结果构造方法只会被执行一次,但指令重排机制会让程序很小的几率出现构造方法被执行多次

==DCL(双端检锁)机制不一定线程安全==,原因时有指令重排的存在,加入volatile可以禁止指令重排

原因是在某一个线程执行到第一次检测,读取到instance不为null时,instance的引用对象可能==没有完成初始化==。instance=new SingleDemo();可以被分为一下三步(伪代码):

memory = allocate();//1.分配对象内存空间
instance(memory);	//2.初始化对象
instance = memory;	//3.设置instance执行刚分配的内存地址,此时instance!=null

步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化时允许的,如果3步骤提前于步骤2,但是instance还没有初始化完成

但是指令重排只会保证串行语义的执行的一致性(单线程),但并不关心多线程间的语义一致性。

==所以当一条线程访问instance不为null时,由于instance示例未必已初始化完成,也就造成了线程安全问题。==

单例模式volatile代码 为解决以上问题,可以将SingletongDemo实例上加上volatile

private static volatile SingletonDemo instance = null;