飞道的博客

【Java】--谈谈你对volatile的理解

487人阅读  评论(0)

1、volatile是JVM提供的轻量级的同步机制

volatile的特性

  • 保证可见性
  • 禁止指令重排序

【JMM】–Java内存模型 一文中,可知为了保证内存可见性,Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。

被volatile关键字修饰的变量,在每个写操作之后,都会加入一条store内存屏障命令,此命令强制工作内存将此变量的最新值保存至主内存;在每个读操作之前,都会加入一条load内存屏障命令,此命令强制工作内存从主内存中加载此变量的最新值至工作内存。

public class TestVolatile {

    public static void main(String[] args) {
        ThreadDemo td = new ThreadDemo();
        new Thread(td).start();

        while(true){
            if(td.isFlag()){
                System.out.println("------------------");
                break;
            }
        }
    }
}

class ThreadDemo implements Runnable {

    private  boolean flag = false;

    @Override
    public void run() {

        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
        }

        flag = true;

        System.out.println("flag=" + isFlag());

    }

    public boolean isFlag() {
        return flag;
    }

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

}

结果如下:

由结果可知,虽然子线程在0.2秒后将flag修改为true,但在main线程中依然为初始值false,即子线程的修改对main线程不可见
在flag 变量前加上关键字volatile后即可增加可见性

  • 不保证原子性
public class MyData {
    volatile int num = 0;
    public void add(){
        num++;
    }
}
public class VolatileDemo {
    public static void main(String[] args) {
        MyData myData = new MyData();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int m = 0; m < 1000; m++) {
                    myData.add();
                }
            },String.valueOf(i) ).start();
        }

        //等待上面计算完成
        while (Thread.activeCount()>2){
            Thread.yield();//礼让
        }

        System.out.println(Thread.currentThread().getName() + "\t finally: " + myData.num);
    }
}

结果:
理论上最后应该为20000,但实际并不是。

这是因为num++被拆分成了4个指令,在多线程情况下出现写覆盖,
即某个线程进行加一操作后写回主存前,另外某个线程读取了旧值,
即使使用了volatile关键字修饰,增加了内存可见性,但并不能保证原子性

原子性解决

  1. 使用synchronized,可以保证原子性,但是每次自增都进行加锁,性能可能会稍微差了点;
  2. 在JUC的子包java.util.concurrent.atomic是一个小型工具包,支持单个变量上的无锁线程安全编程,其中含有许多操作原子数据的类:
public class MyData {
    volatile int num = 0;
    AtomicInteger atomicInteger = new AtomicInteger();
    public void add(){
//        num++;
        atomicInteger.getAndIncrement();
    }
}

2、volatile的用处

  • DCL(双端检锁)版的单例模式
    DCL机制不一定线程安全,存在指令重排的风险,加入volatile可以禁止指令重排序。
    原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化
INSTANCE = new Demo8();// 可以分为以下三步完成(伪代码)

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

由于步骤2和3不存在数据依赖关系,而且无论重拍前还是重拍后程序的结果再单线程中没有改变,因此这种重排优化是允许的。

public class Demo8 {
    private static volatile Demo8 INSTANCE; //JIT

    private Demo8() {
    }

    public static Demo8 getInstance() {
        if (INSTANCE == null) {
            //双重检查
            synchronized (Demo8.class) {
                if(INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Demo8();
                }
            }
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->
                    System.out.println(Thread.currentThread().getName()+"\t"+Demo8.getInstance().hashCode())
                    ,String.valueOf(i)).start();
        }
    }
}

转载:https://blog.csdn.net/weixin_44371151/article/details/106626494
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场