并发编程 Voliatile

211 阅读4分钟

对一个技术人的定义什么,是钢铁直男,还是情商极低,不会说话,或者是专注技术有所产出,充满了认真的魅力,我们说事物总有两面性,其实事物是有多面性,如果不能拥有上帝视角就很难做好一件事,而我们能做的就是尽量去拥有这种视角,全面的看待问题,这就需要很大的知识储备。

从今天开始要写一些并发编程的东西了,并发编程其实也用到过,可是的确很少,工作中大部分时间是crud,都是写大众的东西,只会拿出10%的时间去写类似并发的东西,10%才是精华,现在就开始说说精华之一volatile。

1 volatile的作用和使用场景

volatile 处理线程间变量的可见性的以及防止指令重排,用代码验证一下

  /**
 * A B两个线程都在跑  他两们对同一个变量做修改  但是由于个线程间变量是不可见的 所以B线程修改了变量不影响
 * 导致 这段代码会一直输出你坏 不会输出你好 (不加修饰时)
 * 更正下其实也不是不会输出 jvm其实会尽量保证可见性的
 * @param args
 */
public static void main(String[] args) {

    Optional.empty();

    final VT vt = new VT();

    Thread Thread01 = new Thread(vt);
    Thread Thread02 = new Thread(new Runnable() {
        public void run() {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException ignore) {
            }
            vt.sign = true;
            System.out.println("vt.sign = true 通知 while (!sign) 结束!");
        }
    });

    Thread01.start();
    Thread02.start();
}

}

/**
 * volatile原理
 * 实际上 内存分为一个主内存 然后每增加一个线程 就会开辟一个分内存,两个线程之间的变量都是在各自的分内存上
 * 如果不加volatile  那么A工作内存 和B工作内存中 有各自的变量 sign 一个是true 一个是false sign属性的改变不会修改主内存
 * 加上volatile 那么会强制主内存进行刷新,过期内存,线程B的内存会获取最新的主内存,进而实现了线程间变量的可见性
 *
 * 那他是怎么实现这个过程的,通过查看汇编指令发现多了一个lock
 * lock指令相当于一个内存屏障,它保证如下三点:
 *
 * 将本处理器的缓存写入内存。
 * 重排序时不能把后面的指令重排序到内存屏障之前的位置。
 * 如果是写入动作会导致其他处理器中对应的内存无效。
 *
 * 加上volatile并不能保证原子性
 */

class VT implements Runnable {
    private Logger logger = LoggerFactory.getLogger(Volidate.class);
    //验证volatile
    public volatile boolean sign = false;

    public void run() {
        while (true) {
            if (sign) {
                logger.info("你好");
            } else {
                logger.info("你坏");
            }
        }
    }

}

使用场景,比如单例模式下

**
 * 验证有序性
 * volatile 不仅有线程间可见性的功能
 * 还有防止指令重排的功能 而sync是没有这个功能的
 */
public class Singleton {
    private Singleton() {
    }

    /**
     * 防止指令重排
     */
    private volatile static Singleton instance;

    public Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                //这个时候 指令重拍就会让这个判断出错
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

}

2 volatile是怎样实现的指令重排

通过查看汇编指令,发现汇编执行中有一个lock,这个lock最终操作内存屏障,达到了防止指令重排的目的

3 什么时候发生指令重排

举个栗子

instance= new Singleton(),是一个原子操作,其实际上可以抽象为下面几条JVM指令
memory =allocate();    //1:分配对象的内存空间
ctorInstance(memory);  //2:初始化对象
instance =memory;     //3:设置instance指向刚分配的内存地址

上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的 优化重排序的,经过重排序后如下:

memory =allocate();    //1:分配对象的内存空间
instance =memory;     //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory);  //2:初始化对象

可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。

说到现在,发现其实volatile是为了多线程准备的,但是他就不是为高并发准备的,因为他没有原子性,使用他的场景,一定是不依赖他当前值的场景,或者用他防止指令重排的功能,当然防止指令重排相当于也就防止了jvm的优化有些消耗性能。