JavaSE-多线程(3)- volatile

JavaSE-多线程(3)- volatile

volatile 解释

volatile 字面意思为 易变的,解释具体作用前可先看下以下例子:
VolatileTest类中有一个running 变量控制run方法执行,t1 线程启动后 run 方法一直执行,主线程在等待2秒后改变running 值,试图让 t1 线程结束运行,但通过运行结果发现效果并没有达到

例1)

package com.hs.example.base.multithread.day01;

public class VolatileTest {

    private /*volatile*/ boolean running = true;

    public void run(){
        System.out.println("start...");
        while (running){
        }
        System.out.println("end...");
    }

    public static void main(String[] args) {
        VolatileTest volatileTest = new VolatileTest();
        Thread t1 = new Thread(volatileTest::run);
        t1.start();
        /*new Thread(new Runnable() {
            @Override
            public void run() {
                volatileTest.run();
            }
        }).start();*/
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        volatileTest.running = false;
    }
}


但是如果在变量running 前加上 volatile关键字,情况就会和预期的一样,通过在主线程改变running值,t1 线程可以停止运行

为什么出现以上情况

VolatileTest对象实例存放于堆内存中,而 main 和 t1 线程有自己的工作空间,他们访问的 running 变量实际是存放于自己工作空间的副本,当main线程改变自己工作空间 running 变量的值时,堆内存无法及时更新,t1 线程无法拿到最新的 running 值,所以导致线程一直运行。

例2)

下例是一个“懒汉式”的单例模式

package com.hs.example.base.multithread.day01;

public class VolatileTest2 {

    private static /*volatile*/ VolatileTest2 instance = null;

    private int a = 4;

    private VolatileTest2() {
    }

    public static VolatileTest2 getInstance() {
        //双重空值检查
        //步骤 1  当instance实例化后可过滤大部分情况,直接返回实例
        if (instance == null) {
            //步骤 2
            synchronized (VolatileTest2.class) {
                /**
                 * 步骤 3  第二次空值判断,
                 * 假设两个线程 t1 , t2 同时到达步骤1,两者都判断实例为空,那么t1 获得锁后实例化了一个对象,等到t2 获得锁后又会实例化一个对象
                 * 在这里再加一层判断后,t1实例化对象,t2 获得锁,再判断一次,就不会再实例化了
                 */
                if (instance == null) {
                    instance = new VolatileTest2();
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                System.out.println(VolatileTest2.getInstance());
            }).start();
        }
    }
}

上面例子看似没什么问题,但是可能存在多线程访问时,VolatileTest2 实例属性值不一致问题,究其原因可以了解下对象创建过程。

对象创建过程

  • ①给对象分配内存
  • ②给对象属性赋默认值(以上a的默认值为0)
  • ③给对象属性赋初始值(a = 4)
  • ④将对象赋值给引用 instance

假设对象创建时,还没有赋初始值就将对象赋值给了引用,那么这时候 instance 拿到的变量值就不是正确的(此时对象初始化还没完成),如果此时有线程进入,判断 instance 不为空,那么使用的 instance 属性值就是有误的,使用 volatile 关键字可以避免这种情况发生

volatile 作用
  • 保证线程可见性 (例1)
  • 防止指令重排序 (例2)
volatile 为什么能保证线程可见性

当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程将只能从主内存中重新读取共享变量,其内存语义实现则是通过内存屏障。

内存屏障有两个指令: Load Barrier(读屏障),Store Barrier(写屏障)

内存屏障的作用

  • 阻止屏障两侧的指令重排序:(后面不往屏障前重排,前面不往屏障后重排)
  • 强制把写缓冲区、高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
  • 对于 Load Barrier 来说,在指令前插入 Load Barrier,强制重新从主内存加载数据,可以让高速缓存中的数据失效(不去读缓存中的数据,而是去主内存中重新加载)
  • 对于 Store Barrier 来说,在指令后插入 Store Barrier, 能让写入缓存中的最新数据更新写入主内存,让其他线程可见。

四种内存屏障:

LoadLoad屏障:禁止读和读的重排序
StoreStore屏障:禁止写和谐的重排序
LoadStore屏障:禁止读和写的重排序
StoreLoad屏障:禁止写和读的重排序

可以通过以下文章了解详细内容:

https://baijiahao.baidu.com/s?id=1709086005694976168&wfr=spider&for=pc

https://blog.csdn.net/weixin_43093006/article/details/111351514

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值