volatile可见性案例执行失败的原因分析

一般情况下大家在网上看到关于volatile关键字的解释有如下两点

1. 内存可见性

2. 防止指令重排序

对于1.内存可见性,通常都会贴上如下代码,来证明没有volatile关键字修饰的变量被线程A修改后,线程B未感知到

public class ThreadDemo extends Thread {

    private boolean isRunning = true;

    public boolean isRunning() {
        return isRunning;
    }

    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }

    @Override
    public void run() {
        System.out.println("进入了run");
        while (isRunning) {
            
        }
        System.out.println("结束");
    }

    public static void main(String[] args) {
        try {
            ThreadDemo t = new ThreadDemo();
            t.start();
            Thread.sleep(100);
            t.setRunning(false);
            System.out.println("isRunning设置成false");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行结果

isRunning设置为false,未被感知到,线程一直没有停止。

此时给isRunning增加volatile修饰

private volatile boolean isRunning = true;

 程序可以正常结束

但是当你自己手写这块代码时,你有时候会发现情况并不会这样,比如这样

    @Override
    public void run() {
        System.out.println("进入了run");
        while (isRunning) {
            System.out.println("===");
        }
        System.out.println("结束");
    }

 我在while代码里添加了一个System.out.println("==="), 我就想看一下进程一直在里边转,一直在打印输出东西,特此强调,此时的isRunning仍未修饰volatile,完整代码是这样的

public class ThreadDemo extends Thread {

    private boolean isRunning = true;

    public boolean isRunning() {
        return isRunning;
    }

    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }

    @Override
    public void run() {
        System.out.println("进入了run");
        while (isRunning) {
            System.out.println("===");
        }
        System.out.println("结束");
    }

    public static void main(String[] args) {
        try {
            ThreadDemo t = new ThreadDemo();
            t.start();
            Thread.sleep(100);
            t.setRunning(false);
            System.out.println("isRunning设置成false");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行结果看

???怎么回事,线程感知到了isRunning的变化,并终止了循环体,结束了线程,就因为在循环体里加了一个 System.out.print()。

(PS: 这个现象在网上几乎所有关于volatile的关键字分析案例中都进行了规避处理,包括马士兵的视频教程,即使他在while代码块里写了个System.out.print()发现了案例不正常,他也选择把代码注释掉然后继续讲课)

打开System.out.print()源码,发现里面有synchronized代码块,继而发现直接添加synchronized代码块也可以复现这个问题,既然synchronized可以复现,那尝试时候会发现Lock和AQS系的锁都可以复现。那我们知道AQS的原理是Unsafe类,但是通过尝试发现并不是所有用到Unsafe类的都会有这样的情况,比如Atomic系的对象创建并不会复现该问题,但是Atomic系对象的增减是可以复现的。synchronized和Unsafe自旋底层原理都是lock cmpxchg,原子类底层原理是lock addl,volatile原理也是lock,所以是否跟lock有关,而lock是一个内核态操作,这中间是否与线程上下文切换有关,我们知道Thread.sleep和Thread.yield一个是线程阻塞,一个是让出CPU资源,两个操作都会产生CPU内核态切换。

可重现该现象的代码有如下这些方式:

  1. System.out.print() (方法中使用了synchronized锁)
  2. synchronized(){}
  3. new AbstractQueuedSynchronizer(){} (包括用到AQS系的子类Sync的API锁,如Lock系的ReentrantLock、ReentrantReadWriteLock,Semaphore,CountDownLatch等)
  4. 原子类的数据变化
  5. new File("/xx")
  6. Thread.sleep()
  7. Thread.yield()
  8. 其他未探索发现的方式

仔细分析一下上述代码的特征: 

  1. synchronized底层需要内核态切换执行lock cmpxchg操作
  2. AQS系的Unsafe自旋一样会用到lock操作,
  3. 原子类实现原理还是Unsafe自旋,
  4. Thread.sleep和yeild,自然不用说,线程阻塞和让出CPU使用权一定会产生线程切换即产生CPU中断。
  5. File类也会涉及到Unsafe自旋操作

结论:可能的原因是因为上述几种方式均出现了线程上下文切换即产生了CPU中断,线程切换之后共享变量会被同步更新到工作内存中,所以会出现未添加volatile关键字,修改仍可见的现象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值