关于volatile:深入理解关键字volatile

volatile 关键字的作用是什么?相比于 synchronized 关键字(重量级锁)对性能影响较大,Java提供了一种较为轻量级的可见性和有序性问题的解决方案,那就是应用 volatile 关键字。因为应用 volatile 不会引起上下文的切换和调度,所以 volatile 对性能的影响较小,开销较低。 从并发三要素的角度看,volatile 能够保障其润饰的变量的可见性和有序性,无奈保障原子性(不能保障齐全的原子性,只能保障单次读/写操作具备原子性,即无奈保障复合操作的原子性)。 上面将从并发三要素的角度介绍 volatile 如何做到可见和有序的。 1. volatile 如何实现可见性?什么是可见性?可见性指当多个线程同时访问共享变量时,一个线程对共享变量的批改,其余线程能够立刻看到(即任意线程对共享变量操作时,变量一旦扭转所有线程立刻能够看到)。 1.1 可见性例子/** * volatile 可见性例子 * @author 单程车票 */public class VisibilityDemo { // 结构共享变量 public static boolean flag = true;// public static volatile boolean flag = true; // 如果应用volatile润饰则能够终止循环 public static void main(String[] args) { // 线程1更改flag new Thread(() -> { // 睡眠3秒确保线程2启动 try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();} // 批改共享变量 flag = false; System.out.println("批改胜利,以后flag为true"); }, "one").start(); // 线程2获取更新后的flag终止循环 new Thread(() -> { while (flag) { } System.out.println("获取到批改后的flag,终止循环"); }, "two").start(); }}不应用 volatile 润饰 flag 变量时,运行程序会进入死循环,也就是说线程1对 flag 的批改并没有被线程2读到,也就是说这里的flag并不具备可见性。应用 volatile 润饰 flag 变量时,运行程序会终止循环,打印提醒语句,阐明线程2读到了线程1批改后的数据,也就是说被 volatile 润饰的变量具备可见性。*1.2 volatile 如何保障可见性?被 volatile 润饰的共享变量 flag 被一个线程批改后,JMM(Java内存模型)会把该线程的CPU内存中的共享变量 flag 立刻强制刷新回主存中,并且让其余线程的CPU内存中的共享变量 flag 缓存生效,这样当其余线程须要拜访该共享变量 flag 时,就会从主存获取最新的数据。 ...

March 20, 2023 · 3 min · jiezi

关于volatile:Java中不可或缺的关键字volatile

什么是volatile关键字volatile是Java中用于润饰变量的关键字,其能够保障该变量的可见性以及程序性,然而无奈保障原子性。更精确地说是volatile关键字只能保障单操作的原子性, 比方x=1 ,然而无奈保障复合操作的原子性,比方x++ 其为Java提供了一种轻量级的同步机制:保障被volatile润饰的共享变量对所有线程总是可见的,也就是当一个线程批改了一个被volatile润饰共享变量的值,新值总是能够被其余线程立刻得悉。相比于synchronized关键字(synchronized通常称为重量级锁),volatile更轻量级,开销低,因为它不会引起线程上下文的切换和调度。 保障可见性可见性:是指当多个线程拜访同一个变量时,一个线程批改了这个变量的值,其余线程可能立刻看到批改的值。咱们一起来看一个例子: public class VisibilityTest { private boolean flag = true; public void change() { flag = false; System.out.println(Thread.currentThread().getName() + ",已批改flag=false"); } public void load() { System.out.println(Thread.currentThread().getName() + ",开始执行....."); int i = 0; while (flag) { i++; } System.out.println(Thread.currentThread().getName() + ",完结循环"); } public static void main(String[] args) throws InterruptedException { VisibilityTest test = new VisibilityTest(); // 线程threadA模仿数据加载场景 Thread threadA = new Thread(() -> test.load(), "threadA"); threadA.start(); // 让threadA执行一会儿 Thread.sleep(1000); // 线程threadB 批改 共享变量flag Thread threadB = new Thread(() -> test.change(), "threadB"); threadB.start(); }}其中:threadA 负责循环,threadB负责批改 共享变量flag,如果flag=false时,threadA 会完结循环,然而下面的例子会死循环! 起因是threadA无奈立刻读取到共享变量flag批改后的值。 咱们只需private volatile boolean flag = true;,加上volatile关键字threadA就能够立刻退出循环了。 ...

January 6, 2023 · 3 min · jiezi

关于volatile:volatilesynchronized可见性有序性原子性代码证明基础硬核

0.简介前一篇文章《Synchronized用法原理和锁优化降级过程》从面试角度详细分析了synchronized关键字原理,本篇文章次要围绕volatile关键字用代码剖析下可见性,原子性,有序性,synchronized也辅助证实一下,来加深对锁的了解。** 1.可见性 1.1 不可见性A线程操作共享变量后,该共享变量对线程B是不可见的。咱们来看上面的代码。 package com.duyang.thread.basic.volatiletest;/** * @author :jiaolian * @date :Created in 2020-12-22 10:10 * @description:不可见性测试 * @modified By: * 公众号:叫练 */public class VolatileTest { private static boolean flag = true; public static void main(String[] args) throws InterruptedException { Thread threadA = new Thread(() -> { while (flag){ //留神在这里不能有输入 }; System.out.println("threadA over"); }); threadA.start(); //休眠100毫秒,让线程A先执行 Thread.sleep(100); //主线程设置共享变量flag等于false flag = false; }}上述代码中,在主线程中启动了线程A,主线程休眠100毫秒,目标是让线程A先执行,主线程最初设置共享变量flag等于false,控制台没有输入后果,程序死循环没有完结不了。如下图所示主线程执行完后flag = false后Java内存模型(JMM),主线程把本人工作内存的flag值设置成false后同步到主内存,此时主内存flag=false,线程A并没有读取到主内存最新的flag值(false),主线程执行结束,线程A工作内存始终占着cpu工夫片不会从主内存更新最新的flag值,线程A看不到主内存最新值,A线程应用的值和主线程应用值不统一,导致程序凌乱,这就是线程之间的不可见性,这么说你应该能明确了。线程间的不可见性是该程序死循环的根本原因。 1.2 volatile可见性上述案例中,咱们用代码证实了线程间的共享变量是不可见的,其实你能够从上图得出结论:只有线程A的工作内存可能感知主内存中共享变量flag的值发生变化就好了,这样就能把最新的值更新到A线程的工作内存了,你只有能想到这里,问题就曾经完结了,没错,volatile关键字就实现了这个性能,线程A能感知到主内存共享变量flag产生了变动,于是强制从主内存读取到flag最新值设置到本人工作内存,所以想要VolatileTest代码程序失常完结,用volatile关键字润饰共享变量flag,private volatile static boolean flag = true;就功败垂成。volatile底层实现的硬件根底是基于硬件架构和缓存一致性协定。如果想深刻下,能够翻看上一篇文章《可见性是什么?(通俗易懂)》。肯定要试试才会有播种哦! ...

December 22, 2020 · 5 min · jiezi

关于volatile:面经手册-第14篇volatile-怎么实现的内存可见没有-volatile-一定不可见吗

作者:小傅哥博客:https://bugstack.cn 积淀、分享、成长,让本人和别人都能有所播种!????一、码场心得 你是个能享乐的人吗? 从前的能享乐大多指的体力劳动的苦,但当初的能享乐曾经包含太多维度,包含:读书学习&寂寞的苦、深度思考&脑力的苦、自律习惯&修行的苦、自控能力&放弃的苦、抬头做人&尊严的苦。 尽管这些苦摆在眼前,但大多数人还是喜爱吃简略的苦。熬夜加班、日复一日、反复昨天、CRUD,最初身材发胖、体质降落、能力有余、自抱自泣!所以有些苦能不吃就不吃,要吃就吃那些有成长价值的苦。 明天你写博客了吗? 如果一件小事能保持5年以上,那你肯定是很了不起的人。是的,很了不起。人最难的就是想分明了但做不到,或者偶然做到长期做不到。 其实大多数走在研发路上的搭档们,都晓得本人该致力,但明明下好了的信心就是保持不了多久。就像你是否也想过要写技术博客,做技术积攒。直到有一天被瓶颈限度在困局中才会焦急,但这时候在想破局就真的很难了! 二、面试题谢飞机,小记,飞机趁着周末,吃完火锅。又去约面试官喝茶了! 谢飞机:嗨,我在这,这边,这边。 面试官:你怎么又来了,最近学的不错了? 谢飞机:还是想来大厂,别害羞,面我吧! 面试官:我如同是你补课老师... 既然来了,就问问你吧!volatile 是干啥的? 谢飞机:啊,volatile 是保障变量对所有线程的可见性的。 面试官:那 volatile 能够解决原子性问题吗? 谢飞机:不能够! 面试官:那 volatile 的底层原理是如何实现的呢? 谢飞机:...,这!面试官,刚问两个题就甩雷,你是不家里有事要忙? 面试官:你管我! 三、volatile 解说1. 可见性案例public class ApiTest { public static void main(String[] args) { 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(); }}class VT implements Runnable { public boolean sign = false; public void run() { while (!sign) { } System.out.println("你坏"); }}这段代码,是两个线程操作一个变量,程序冀望当 sign 在线程 Thread01 被操作 vt.sign = true 时,Thread02 输入 你坏。 ...

October 22, 2020 · 3 min · jiezi

关于volatile:理解Volatile关键字其实看这一篇就够了写的非常细致

前言volatile是Java虚拟机提供的轻量级的同步机制。 volatile关键字作用是什么?两个作用: 1.保障被volatile润饰的共享变量对所有线程总数可见的,也就是当一个线程批改了一个被volatile润饰共享变量的值,新值总是能够被其余线程立刻得悉。 2.禁止指令重排序优化。 volatile的可见性对于volatile的可见性作用,咱们必须意识到被volatile润饰的变量对所有线程总数立刻可见的,对volatile变量的所有写操作总是能立即反馈到其余线程中; 上面来测试一下,此时的还未initFlag被volatile润饰。 private boolean initFlag = false; public void test() throws InterruptedException{ Thread threadA = new Thread(() -> { while (!initFlag) { } String threadName = Thread.currentThread().getName(); System.out.println("线程" + threadName+"获取到了initFlag扭转后的值"); }, "threadA"); //线程B更新全局变量initFlag的值 Thread threadB = new Thread(() -> { initFlag = true; }, "threadB"); //确保线程A先执行 threadA.start(); Thread.sleep(2000); threadB.start();}执行后果:控制台只打印了 "线程threadB扭转了initFlag的值",且程序并未终止。 此时initFlag曾经被volatile关键字润饰了 private volatile boolean initFlag = false; public void test() throws InterruptedException{ Thread threadA = new Thread(() -> { while (!initFlag) { } String threadName = Thread.currentThread().getName(); System.out.println("线程" + threadName+"获取到了initFlag扭转后的值"); }, "threadA"); Thread threadB = new Thread(() -> { initFlag = true; String threadName = Thread.currentThread().getName(); System.out.println("线程" + threadName+"扭转了initFlag的值"); }, "threadB"); //确保线程A先执行 threadA.start(); Thread.sleep(2000); threadB.start();}执行后果: ...

August 27, 2020 · 2 min · jiezi

关于volatile:volatile域的语义及其实现

0.背景-缓存一致性依据维基百科的定义:在一个共享内存多处理器零碎中,每个处理器都有一个独自的缓存,能够有很多共享数据正本:一个在主内存中,一个在每个申请它的处理器的本地缓存中。 当一个数据正本被更改时,其余正本必须反映该更改。 缓存一致性是确保共享操作数(数据)值的更改及时在整个零碎中流传的学科。上面图1是缓存不统一的示例图,图2是缓存统一的示例图其实Java的volatile某种意义上也是来解决这种缓存不统一的状况。 更多缓存一致性的常识,能够参看维基百科的词条,也能够看medium上的这篇文章 1.JMM提供的volatile域的语义1.1 可见性依据JSR-133 FAQ中的阐明,volatile字段是用于在线程之间传递状态的非凡字段。 每次读取volatile时,都会看到任意一个线程对该volatile的最初一次写入。 实际上,程序员将volatile字段指定为不能承受因为缓存或重排序而导致的“过期”值的字段。 禁止编译器和运行时在寄存器中调配它们。 它们还必须确保在写入后将其从高速缓存(cache)中刷新到主存(memory),以便它们能够立刻对其余线程可见。 同样,在读取volatile字段之前,必须使高速缓存有效,以便能够看到主内存中的值而不是本地处理器高速缓存中的值。 也就是说每次读取volatile都是从主存读取,写入也会刷新到主存,因此保障了不同线程拿到的都是最新值,即保障了共享资源对各个CPU上的线程的可见性,这其实就是保障了缓存一致性。 1.2. 重排序限度在旧的内存模型下(Java1.5之前),对volatile变量的访问不能互相重排序,但能够与nonvolatile变量拜访一起重排序。 这毁坏了volatile字段作为从一个线程到另一线程发信号告诉状态的一种伎俩。 在新的内存模型下(Java1.5及之后),volatile变量不能互相从新排序依然为true。区别在于,当初对它们四周的失常字段拜访进行重排序不再那么容易了。 写入一个volatile 字段具备与monitor开释雷同的存储成果,而从一个volatile 字段中读取具备与monitor获取雷同的存储成果。 实际上,因为新的内存模型对volatile 字段拜访与其余字段拜访(无论是否为易失性)的从新排序施加了更严格的束缚,因而当线程A写入volatile 字段f时,对线程A可见的任何内容,这些内容在线程B读取f时都可见。 这是一个如何应用易失性字段的简略示例: class VolatileExample { int x = 0; volatile boolean v = false; public void writer() { x = 42; v = true; } public void reader() { if (v == true) { //uses x - guaranteed to see 42. } }}假设一个线程在调用writer办法,而另一个在调用reader办法。在writer办法中对v的写操作,会将对x的写操作也更新到主存中,而对v的读操作则从主存中获取该值。 因而,如果reader办法看到v的值为true,那么也保障能够看到在它之前产生的对42的写入。 ...

August 8, 2020 · 3 min · jiezi

深入理解-Volatile-的实现原理

Volatile 的官方定义Java 语言规范第三版中对 volatile 的定义如下: java 编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java 语言提供了 volatile,在某些情况下比锁更加方便。如果一个字段被声明成 volatile,java 线程内存模型确保所有线程看到这个变量的值是一致的。 什么情况下可使用 volatile在多线程并发编程时,为了保持共享变量在多个线程的一致性。 =》 可见性为了保证代码执行按编码的顺序执行。 =》 有序性并发编程中的三个特性:原子性,有序性和可见性。volatile就作用了其中的两个。 为什么使用 volatile恰当的使用,它的使用和执行成本比synchronized更低,因为不会引起线程上下文的切换和调度。 volatile的实现原理是什么volatile 是依赖于硬件层面的支持,即需要 CPU 的指定来实现。 对于volatile修饰的变量,在汇编语言层面会多一行指令 0x01a3de24: lock addl $0x0,(%esp);。而该lock指令通过查IA-32架构可知主要做两件事 : 将当前处理器缓存行的数据写回到系统内存中。该写回内存操作会引起其他CPU里缓存了该内存地址的数据失效。 =》 CPU 的嗅探机制。详细原理如下: 处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2 或其他)后再进行操作,但操作完之后不知道何时会写到内存,如果对声明了 Volatile 变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存。 但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。 LOCK 前缀指令的改进Lock 前缀指令会引起处理器缓存回写到内存。 Lock 前缀指令导致在执行指令期间,声言处理器的 LOCK# 信号,在该信号期间,会独占使用任何共享内存 第一阶段: LOCK指令会锁住总线,导致其他的处理器不能访问总线,也就不能访问系统内存。将多线程的并发变成了串行执行。优化后 LOCK指令不再锁总线,而是锁缓存行。并将数据会写到该缓存,使用缓存一致性来保证原子性。缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。 新的 CPU 会使用 MESI(修改,独占,共享,无效)控制协议来维护内部缓存和其他处理器的缓存的一致性。 可以看出硬件技术的进步对于软件的性能提升有质的飞越。 volatile在软件层面的优化并发编程大师 Doug lea在 JDK1.7 中新增了队列集合类 LinkedTransferQueue,在使用Volatile时用追价字节的方式优化队列出栈和入栈的性能。 为什么追加 64 字节能够提高并发编程的效率呢?因为对于英特尔酷睿 i7,酷睿, Atom 和 NetBurst, Core Solo 和 Pentium M 处理器的 L1,L2 或 L3 缓存的高速缓存行是 64 个字节宽,不支持部分填充缓存行,这意味着如果队列的头节点和尾节点都不足 64 字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头尾节点,当一个处理器试图修改头接点时会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作是需要不停修改头接点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。Doug lea 使用追加到 64 字节的方式来填满高速缓冲区的缓存行,避免头接点和尾节点加载到同一个缓存行,使得头尾节点在修改时不会互相锁定。 ...

October 14, 2019 · 1 min · jiezi

Java设计模式优化单例模式

单例模式概述单例模式是一种对象创建模式,用于产生一个类的具体事例。使用单例模式可以确保整个系统中单例类只产生一个实例。有下面两大好处: 对于频繁创建的对象,节省初第一次实例化之后的创建时间。由于new操作的减少,会降低系统内存的使用频率。减轻GC压力,从而缩短GC停顿时间创建方式: 单例作为类的私有private属性单例类拥有私有private构造函数提供获取实例的public方法单例模式的角色: 角色作用单例类提供单例的工厂,返回类的单例实例使用者获取并使用单例类类基本结构: 单例模式的实现1.饿汉式public class HungerSingleton { //1.饿汉式 //私有构造器 private HungerSingleton() { System.out.println("create HungerSingleton"); } //私有单例属性 private static HungerSingleton instance = new HungerSingleton(); //获取单例的方法 public static HungerSingleton getInstance() { return instance; }}注意: 单例修饰符为static JVM加载单例类加载时,直接初始化单例。无法延时加载。如果此单例一直未被使用,单Singleton 因为调用静态方法被初始化则会造成内存的浪费。getInstance()使用static修饰,不用实例化可以直接使用Singleton.getInstance()获取单例。由于单例由JVM加载类的时候创建,所以不存在线程安全问题。2.简单懒汉式public class Singleton { //2.1简单懒汉式(线程不安全) //私有构造器 private Singleton() { System.out.println("create Singleton"); } //私有单例属性[初始化为null] private static Singleton instance = null; //获取单例的方法 public static Singleton getInstance() { if(instance == null) { //此处instance实例化 //首次调用单例时会进入 达成延时加载 instance = new Singleton(); } return instance; }}由于未使用 synchronized 关键字,所以当线程1调用单例工厂方法Singleton.getInstance() 且 instance 未初始化完成时,线程2调用此方法会将instance判断为null,也会将instance重新实例化赋值,此时则产生了多个实例!如需线程安全可以直接给getInstance方法上加synchronized关键字,如下:public class Singleton { //2.2简单懒汉式(线程安全) //私有构造器 private Singleton() { System.out.println("create Singleton"); } //私有单例属性[初始化为null] private static Singleton instance = null; //获取单例的方法 将此方法使用synchronized关键字同步 public static synchronized Singleton getInstance() { if(instance == null) { //此处instance实例化 //首次调用单例时会进入 达成延时加载 instance = new Singleton(); } return instance; }}面临的问题: ...

August 7, 2019 · 2 min · jiezi

单例终极分析一

单例的用处如果你看过设计模式,肯定会知道单例模式,实际上这是我能默写出代码的第一个设计模式,虽然很长一段时间我并不清楚单例具体是做什么用的。这里简单提一下单例的用处。作为java程序员,你应该知道spring框架,而其中最核心的IOC,在默认情况下注入的Bean就是单例的。有什么好处?那些Service、Dao等只创建一次,不必每次都通过new方式创建,也就不用每次都开辟空间、垃圾回收等等,会省不少资源。 version 1: 饿汉式那么如何写一个单例呢?我想很多朋友都能搞定: public class Singleton { private static final Singleton singletonInstance = new Singleton(); // A - 急不可待的成员变量赋值,static和final修饰 private Singleton (){} // B - 私有化的构造器,避免随意new public static Singleton getInstance(){ // C - 暴露给外部的获取方法 return singletonInstance; }}Ok,拥有A、B、C三大特点(注释部分),就构成了著名的饿汉式单例。好处在于简单粗暴,易于理解(只要你真正通晓final和static的作用)。但有豪放派,就有婉约派。后来大家都觉得,我还没有使用这个类,你就直接把对象构建出来扔java堆里了,是不是有点不那么含蓄? 于是大家快速迭代出懒汉式单例。 version 2: 懒汉式class Singleton { private static Singleton singletonInstance; // A - 温婉到只有变量声明 private Singleton (){} // B public static Singleton getInstance(){ // C if(singletonInstance==null){ singletonInstance = new Singleton(); // D - 成员变量的创建赋值延后至此 } return singletonInstance; }}变化发生于A、D两步,总得来说,就是把成员变量singletonInstance的创建和赋值延后了。基本的要求达到了,在没调用getInstance()方法之前,对象无创建,不再麻烦java堆大大。一切看起来都很美好,但仅限于单线程情况下。好,看看大家喜闻乐见的并发场景下,这种简易的写法会出现什么问题——两个线程T-1和T-2同时访问getInstance(),它们都觉得singletonInstance==null判断成立,分别执行了步骤D,成功创建出singletonInstance对象!但是,我们通篇都在聊单例啊,T-1和T-2的玩法无疑很不单例!问题分析出来了,而解决上并不复杂——让线程同步就好。 ...

June 21, 2019 · 2 min · jiezi

重拾 Java 基础

引言最近好久没有遇到技术瓶颈了,思考得自然少了,每天都是重复性的工作。阿里开始招实习,同学问我要不要去申请阿里的实习,我说不去,个人对阿里的印象不好。记得去年阿里给我发了邮件,我很认真地回复,然后他不理我了。(最起码的尊重都没有,就算我菜你起码回复我一下啊?)这种不尊重人的公司感觉去了也不快乐,当程序员最重要的就是快乐,不快乐写什么代码?电话面同学很友好地分享了他的阿里电话面经验。问的都是看功底的问题,和开发经验无关(估计写上个几年代码不写框架应该也不知道这个)。Java中的HashMap、transient、volatile、HTTP301/302、生产者消费者算法。HashMap都问烂了,问的是HashMap的底层原理,我知道你们自己写过JDK,请不要再问我HashMap里的put操作是怎么实现的了!问源码的真的很讨厌,有什么意义吗?看过的就能答上,没看过的就答不上。基础学习transient这并不是第一次听到transient这个词了,之前也用过,当我们使用YunzhiService进行综合查询时,我们会在实体中构造不映射到数据表的属性用于查询。对于这些不映射为数据表字段的属性,我们使用@Transient注解。那Java中被transient修饰又是什么意思呢?为什么要有transient?StackOverflow的解释通俗易懂。The transient keyword in Java is used to indicate that a field should not be part of the serialization (which means saved, like to a file) process.Java中的transient关键字,意味着该字段不参与序列化,意味着不被保存,例如输出到文件中。序列化?fastjson应该用到了这个关键字。厉害厉害,fastjson开发团队基本功扎实。volatile这个关键字也不知道怎么能给大家通俗的讲出来,还是从实际的小例子出发吧?大家回忆一下我们之前的单例模式,单例模式很常用,这个是必须要会的。这是有问题的懒汉模式,多线程的时候就不能保持单例了。public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; }}修正之前的问题纠正一下之前博客中的一个问题,之前这样写虽然也能实现,但是效率极低,因为每次getInstance的时候都会被synchronized阻塞。public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }}为了效率,不能在方法上加锁,所以需要在新建单例的时候加锁,保证只要只有一个单例被new出来。看起来是没问题的,因为我们想当然的以为,一个线程new出来的Singleton,赋值给instance,然后另一个线程获取到的instance就一定不是空。实际上呢?CPU结构让我们来看Youtube上的一张图:在CPU内部结构中,thread-1和thread-2运行在不同的核心上,每个核心有一个local cache,两个线程执行时,会将变量从shared cache读取到local cache。所以thread-1把flag内容改变了,但是thread-2获取的flag还是从local cache中获取的,所以还是true。直到,thread-1的flag更新到shared cache,然后再更新到thread-2的local cache,thread-2才知道flag变了。所以我们的单例也一样,线程A新建了单例,然后其他线程再获取的时候,不一定是线程A所创建的单例对象。拯救世界volatile来拯救世界了。private static volatile Singleton instance;volatile做了两件事,强制将local cache写入到shared cache,同时使其他核心中的local cache对该数据的缓存无效。所以,完整的单例应该是这样:public class Singleton { private static volatile Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }}真正的单例上面的讲解只是为了让大家了解Java中volatile的作用,实际的单例并不这样实现,而是使用私有静态内部类实现懒汉模式,当访问getInstance方法时,才加载Holder类,实例化单例。public class Singleton { private static class Holder { private static Singleton instance = new Singleton(); } private Singleton() { } public static Singleton getInstance() { return Holder.instance; }}呼,长出了一口气,两个晚上了,总算把volatile自己学会然后讲明白了,这个应该是发生的概率很小很小,我为了让volatile的验证让大家都看到,使用JDK自带的线程池,模拟实际的多线程环境,分别执行自己的测试代码,但还是没出现问题。HTTP 301/302去火狐开发者文档看看:301 Moved Permanently永久重定向,请求的资源已经被移动到了由Location头部指定的url上,搜索引擎会根据该响应修正。HTTP升级到HTTPS时应该能用到。302 Found临时重定向,请求的资源被暂时的移动到了由Location头部指定的url上。浏览器会重定向到这个url,但是搜索引擎不会对该资源的链接进行更新。可能会在某个后台服务瘫痪的时候再转给别的后台服务器时用到?生产者消费者至于最后的生产者消费者算法问题,我觉得意义不大,毕竟操作系统的课本出自七位河北工业大学操作系统教师之手。JDK中有阻塞队列,用的就是生产者消费者模型。用到的时候再说吧。总结每个人都是优秀的人,每个人都值得尊敬。软件生而自由,不受世俗污染,不受凡尘打扰,我祝愿每一位软件工程师都能生活在自由、快乐之中。 ...

March 26, 2019 · 1 min · jiezi

Java并发 - volatile/Holder与双重空校验

以下代码摘选自Dubbo框架的ExtensionLoader, 非常有趣:/* ExtensionLoader /@SuppressWarnings(“unchecked”)public T getLoadedExtension(String name) { if (name == null || name.length() == 0) throw new IllegalArgumentException(“Extension name == null”); Holder<Object> holder = cachedInstances.get(name); if (holder == null) { cachedInstances.putIfAbsent(name, new Holder<Object>()); holder = cachedInstances.get(name); } return (T) holder.get();}/ Just a volatile-helper */public class Holder<T> { private volatile T value; public void set(T value) { this.value = value; } public T get() { return value; }}可以看到, 精华就是Holder-volatile和双重null校验的运用;短短几句代码, 蕴含了:volatile关键字的happens-before关系volatile关键字的可见性问题Java多线程的执行路径问题, 及指令重排序的问题;(有兴趣的可自行查阅相关资料, 毕竟上述任意一个都是非常繁杂的话题)这基本上可以作为最佳实践放入自己项目中的多线程模块中了, 在此仅做个记录; ...

March 18, 2019 · 1 min · jiezi

认识CC++ volatile

令人困惑的volatilevolatile字面意思是“不稳定的、易失的”,不少编程语言中存在volatile关键字,也有共同之处,如“表示程序执行期间数据可能会被外部操作修改”,如被外设修改或者被其他线程修改等。这只是字面上给我们的一般性认识,然而具体到不同的编程语言中volatile的语义可能相差甚远。很多人以为自己精通CC++,但是被问起volatile的时候却无法清晰、果断地表明态度,那只能说明还是处在“从入门到精通”的路上,如果了解一门语言常见特性的使用、能够写健壮高效的程序就算精通的话,那实在是太藐视“大师”的存在了。从一个volatile关键字折射出了对CC++标准、编译器、操作系统、处理器、MMU各个方面的掌握程度。几十年的发展,很多开发者因为自己的偏见、误解,或者对某些语言特性(如Java中的volatile语义)的根深蒂固的认识,赋予了CC++ volatile本不属于它的能力,自己却浑然不知自己犯了多大的一个错误。我曾经以为CC++中volatile可以保证保证线程可见性,因为Java中是这样的,直到后来阅读Linux内核看到Linus Torvards的一篇文档,他强调了volatile可能带来的坏处“任何使用volatile的地方,都可能潜藏了一个bug”,我为他的“危言耸听”感到吃惊,所以我当时搜索了不少资料来求证CC++ volatile的能力,事后我认为CC++ volatile不能保证线程可见性。但是后来部门内一次分享,分享中提到了volatile来保证线程可见性,我当时心存疑虑,事后验证时犯了一个错误导致我错误地认为volatile可以保证线程可见性。直到我最近翻阅以前的笔记,翻到了几年前对volatile的疑虑……我决定深入研究下这个问题,以便能顺利入眠。2. 从规范认识volatile以常见的编程语言C、C++、Java为例,它们都有一个关键字volatile,但是对volatile的定义却并非完全相同。Java中对volatile的定义:8.3.1.4. volatile FieldsThe Java programming language allows threads to access shared variables (§17.1). As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables.The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable (§17.4).Java清晰地表达了这样一个观点,Java内存模型中会保证volatile变量的线程可见性,接触过Java并发编程的开发者应该都清楚,这是一个不争的事实。CC++中对volatile的定义:6.7.3 Type qualifiersvolatile: No cacheing through this lvalue: each operation in the abstract semantics must be performed (that is, no cacheing assumptions may be made, since the location is not guaranteed to contain any previous value). In the absence of this qualifier, the contents of the designated location may be assumed to be unchanged except for possible aliasing.C99中也清晰地表名了volatile的语义,不要做cache之类的优化。这里的cache指的是software cacheing,即编译器生成指令将内存数据缓存到cpu寄存器,后续访问内存变量使用寄存器中的值;需要与之作出区分的是hardware cacheing,即cpu访问内存时将内存数据缓存到cpu cache,硬件操作完全对上层应用程序透明。大家请将这两个点铭记在心,要想搞清楚CC++ volatile必须要先理解这里cache的区别。C99清晰吗?上述解释看上去很清晰,但是要想彻底理解volatile的语义,绝非上述一句话就可以讲得清的,C99中定义了abstract machine以及sequence points,与volatile相关的描述有多处,篇幅原因这里就不一一列举了,其中与volatile相关的abstract machine行为描述共同确定了volatile的语义。3. 对volatile持何观点为了引起大家对CC++ volatile的重视并及时表明观点,先贴一个页面“Is-Volatile-Useful-with-Threads”,网站中简明扼要的告知大家,“Friends don’t let friends use volatile for inter-thread communication in C and C++”。But why?isocpp专门挂了这么个页面来强调volatile在不同编程语言中的差异,可见它是一个多么难缠的问题。即便是有这么个页面,要彻底搞清楚volatile,也不是说读完上面列出的几个技术博客就能解决,那也太轻描淡写了,所以我搜索、整理、讨论,希望能将学到的内容总结下来供其他开发者参考,我也不想再因为这个问题而困扰。结合CC++ volatile qualifier以及abstract machine中对volatile相关sequence points的描述,可以确定volatile的语义:不可优化性:不要做任何软件cache之类的优化,即多次访问内存对象时,编译器不能优化为cache内存对象到寄存器、后续访问内存对象转为访问寄存器 [6.7.3 Type qualifiers - volatile];顺序性:对volatile变量的多次读写操作,编译器不能以预测数据不变为借口优化掉读写操作,并且要保证前面的读写操作先于后面的读写操作完成 [5.1.2.3 Program execution];易变性:从不可优化性、顺序性语义要求,不难体会出其隐含着数据“易变性”,这也是volatile字面上的意思,也是不少开发者学习volatile时最熟知的语义;CC++规范没有显示要求volatile支持线程可见性,gcc也没有在标准允许的空间内做什么“发挥”去安插什么保证线程可见性的处理器指令(Java中volatile会使用lock指令使其他处理器cache失效强制读内存保证线程可见性)。而关于CPU cache一致性协议,x86原先采用MESI协议,后改用效率更高的MESIF,都是强一致性协议,在x86这等支持强一致的CPU上,CC++中结合volatile是可以“获得”线程可见性的,在非强一致CPU上则不然。但是CC++ volatile确实是有价值的,很多地方都要使用它,而且不少场景下似乎没有比它更简单的替代方法,下面首先列举CC++ volatile的通用适用场景,方便大家认识volatile,然后我们再研究为什么CC++ volatile不能保证线程可见性。CC++标准中确实没有说volatile要支持线程可见性,大家可以选择就此打住,但是我怀疑的是gcc在标准允许的空间内是怎么做的?操作系统、MMU、处理器是怎么做的?“标准中没有显示列出”,这样的理由还不足以让我停下探索的脚步。4. CC++ need volatileCC++ volatile语义“不可优化型”、“顺序性”、“易变性”,如何直观感受它的价值呢?看C99中给出的适用场景吧。setjmp、longjmp用于实现函数内、函数间跳转(goto只能在函数内跳转),C Spec规定longjmp之后希望跳到的栈帧中的局部变量的值是最新值,而不是setjmp时的值,考虑编译器可能作出一些优化,将auto变量cache到寄存器中,假如setjmp保存硬件上下文的时候恰巧保存了存有该局部变量值的寄存器信息,等longjmp回来的时候就用了旧值。这违背了C Spec的规定,所以这个时候可以使用volatile来避免编译器优化,满足C Spec!signal handler用于处理进程捕获到的信号,与setjmp、longjmp类似,进程捕获、处理信号时需要保存当前上下文再去处理信号,信号处理完成再恢复上下文继续执行。信号处理函数中也可能会修改某些共享变量,假如共享变量在收到信号时加载到了寄存器,并且保存硬件上下文时也保存起来了,那么信号处理函数执行完毕返回(可能会修改该变量)恢复上下文后,访问到的还是旧值。因此将信号处理函数中要修改的共享变量声明为volatile是必要的。设备驱动、Memory-Mapped IO、DMA。我们先看一个示例,假如不使用volatile,编译器会做什么。编译器生成代码可能会将内存变量sum、i放在寄存器中,循环执行过程中,编译器可能认为这个循环可以直接优化掉,sum直接得到了最终的a[0]+a[1]+…a[N]的值,循环体执行次数大大减少。sum = 0;for (i=0; i<N; ++i) sum += a[i];这种优化对于面向硬件的程序开发(如设备驱动开发、内存映射IO)来说有点过头了,而且会导致错误的行为。下面的代码使用了volatile qualifer,其他与上述代码基本相同。如果不存在volatile修饰,编译器会认为最终ttyport的值就是a[N-1],前N-1次赋值都是没必要的,所以直接优化成ttyport = a[N-1]。但是ttyport是外设的设备端口通过内存映射IO得到的虚拟内存地址,编译器发现存在volatile修饰,便不会对循环体中*ttyport = a[i]进行优化,循环体会执行N次赋值,且保证每次赋值操作都与前一次、后一次赋值存在严格的顺序性保证。volatile short ttyport;for (i=0; i<N; ++i) ttyport = a[i];可能大家会有疑问,volatile只是避免编译器将内存变量存储到寄存器,对cpu cache却束手无策,谁能保证每次对ttyport的写操作都确定写回内存了呢?这里就涉及到cpu cache policy问题了。对于外设IO而言,有两种常用方式:Memory-Mapped IO,简称MMIO,将设备端口(寄存器)映射到进程地址空间。以x86为例,对映射内存区域的读写操作通过普通的load、store访存指令来完成,处理器通过内存类型范围寄存器(MTRR,Memory Type Range Regsiters)和页面属性表(PAT,Page Attribute Table)对不同的内存范围设置不同的CPU cache policy,内核设置MMIO类型范围的cpu cache策略为uncacheable,其他RAM类型范围的cpu cache策略为write-back!即直接绕过cpu cache读写内存,但实际上并没有物理内存参与,而是将读写操作转发到外设,上述代码中ttyport = a[i]这个赋值操作绕过CPU cache直达外设。Port IO,此时外设端口(寄存器)采用独立编址,而非Memory-Mapped IO这种统一编址方式,需要通过专门的cpu指令来对设备端口进行读写,如x86上采用的是指令in、out来完成设备端口的读写。而如果是DMA(Direct Memory Access)操作模式的话,它绕过cpu直接对内存进行操作,期间不中断cpu执行,DMA操作内存方式上与cpu类似,都会考虑cpu cache一致性问题。假如DMA对内存进行读写操作,总线上也会对事件进行广播,cpu cache也会观测到并采取相应的动作。如DMA对内存进行写操作,cpu cache也会将相同内存地址的cache line设置为invalidate,后续读取时就可以重新从内存加载最新数据;假如DMA进行内存读操作,数据可能从其他cpu cache中直接获取而非从内存中。这种情况下DMA操作的内存区域,对应的内存变量也应该使用volatile修饰,避免编译器优化从寄存器中读到旧值。以上示例摘自C99规范,通过上述示例、解释,可以体会到volatile的语义特点:“不可优化型、易变性、顺序性”。下面这个示例摘自网络,也比较容易表现volatile的语义特点:// 应为 volatile unsigned int *p = ….unsigned int *p = GetMagicAddress();unsigned int a, b;a = p;b = p;p = a;p = b;GetMagicAddress()返回一个外设的内存映射IO地址,由于unsigned int p指针没有volatile修饰,编译器认为p中的内容不是“易变的”因此可能会作出如下优化。首先从p读取一个字节到寄存器,然后将其赋值给a,然后认为p内容不变,就直接将寄存器中内容再赋值给b。写p的时候认为a == b,写两次没必要就只写了一次。而如果通过volatile对p进行修饰,则就是另一个结果了,编译器会认为p中内容是易变的,每次读取操作都不会沿用上次加载到寄存器中的旧值,而内存映射IO内存区域对应的cpu cache模式又是被uncacheable的,所以会保证从内存读取到最新写入的数据,成功连续读取两个字节a、b,也保证按顺序写入两个字节a、b。相信读到这里大家对CC++ volatile的适用场景有所了解了,它确实是有用的。那接下来我们针对开发者误解很严重的一个问题“volatile能否支持线程可见性”再探索一番,不能!不能!不能!5. CC++ thread visibility5.1. 线程可见性问题多线程编程中经常会通过修改共享变量的方式来通知另一个线程发生了某种状态的变化,希望线程能及时感知到这种变化,因此我们关心“线程可见性问题”。在对称多处理器架构中(SMP),多处理器、核心通过总线共享相同的内存,但是各个处理器核心有自己的cache,线程执行过程中,一般会将内存数据加载到cache中,也可能会加载到寄存器中,以便实现访问效率的提升,但这也带来了问题,比如我们提到的线程可见性问题。某个线程对共享变量做了修改,线程可能只是修改了寄存器中的值或者cpu cache中的值,修改并不会立即同步回内存。即便同步回内存,运行在其他处理器核心上的线程,访问该共享数据时也不会立即去内存中读取最新的数据,无法感知到共享数据的变化。5.2. diff volatile in java、cc++有些编程语言中定义了关键字volatile,如Java、C、C++等,对比下Java volatile和CC++ volatile,差异简直是太大了,我们只讨论线程可见性相关的部分。Java中语言规范明确指出volatile保证内存可见性,JMM存在“本地内存”的概念,线程对“主存”变量的访问都是先加载到本地内存,后续写操作再同步回主存。volatile可以保证一个线程的写操作对其他线程立即可见,首先是保证volatile变量写操作必须要更新到主存,然后还要保证其他线程volatile变量读取必须从主存中读取。处理器中提供了MFENCE指令来创建一个屏障,可以保证MFENCE之前的操作对后续操作可见,用MFENCE可以实现volatile,但是考虑到AMD处理器中耗时问题以及Intel处理器中流水线问题,JVM从MFENCE修改成了LOCK: ADD 0。但是在C、C++规范里面没有要求volatile具备线程可见性语义,只要求其保证“不可优化性、顺序性、易变性”。5.3. how gcc handle volatile这里做个简单的测试:#include <stdio.h>int main() { // volatile int a = 0; int a = 0; while(1) { a++; printf("%d\n", a); } return 0;}不开优化的话,有没有volatile gcc生成的汇编指令基本是一致的,volatile变量读写都是针对内存进行,而非寄存器。开gcc -O2优化时,不加volatile情况下读写操作通过寄存器,加了volatile则通过内存。1)不加volatile :gcc -g -O2 -o main main.c 这里重点看下对变量a的操作,xor %ebx,%ebx将寄存器%ebx设为0,也就是将变量a=0存储到了%ebx,nopl不做任何操作,然后循环体里面每次读取a的值都是直接在%ebx+1,加完之后也没有写回内存。假如有个共享变量是多个线程共享的,并且没有加volatile,多个线程访问这个变量的时候就是用的物理线程跑的处理器核心寄存器中的数据,是无法保证内存可见性的。2)加volatile:gcc -g -O2 -o main main.c这里变量a的值首先被设置到了0xc(%rsp)中,nopl空操作,然后a++时是将内存中的值移动到了寄存器%eax中,然后执行%eax+1再写回内存0xc(%rsp)中,while循环中每次循环执行都是先从内存里面取值,更新后再写回内存。但是这样就可以保证线程可见性了吗?No!5.4. how cpu cache works是否有这样的疑问?CC++中对volatile变量读写,发出的内存读写指令不会被CPU转换成读写CPU cache吗?这个属于硬件层面内容,对上层透明,编译器生成的汇编指令也无法反映实际执行情况!因此,只看上述反汇编示例是不能确定CC++ volatile支持线程可见性的,当然也不能排除这种可能性?Stack Overflow上Dietmar Kühl提到,‘volatile’阻止了对变量的优化,例如对于频繁访问的变量,会阻止编译器对其进行编译时优化,避免将其放入寄存器中(注意是寄存器而不是cpu的cache)。编译器优化内存访问时,会生成将内存数据缓存到寄存器、后续访问内存操作转换为访问寄存器,这称为“software cacheing”;而CPU实际执行时硬件层面将内存数据缓存到CPU cache中,这称为“hardware cacheing”,是对上层完全透明的。现在已经确定CC++ volatile不会再作出“将内存数据缓存到CPU寄存器”这样的优化,那上述CPU hardware caching技术就成了我们下一个怀疑的对象。保证CPU cache一致性的方法,主要包括write-through(写直达)或者write-back(写回),write-back并不是当cache中数据更新时立即写回,而是在稍后的某个时机再写回。写直达会严重降低cpu吞吐量,所以现如今的主流处理器中通常采用写回法,而写回法又包括了write-invalidate和write-update两种方式,可先跳过。write-back:write-invalidate,当某个core(如core 1)的cache被修改为最新数据后,总线观测到更新,将写事件同步到其他core(如core n),将其他core对应相同内存地址的cache entry标记为invalidate,后续core n继续读取相同内存地址数据时,发现已经invalidate,会再次请求内存中最新数据。write-update,当某个core(如core 1)的cache被修改为最新数据后,将写事件同步到其他core,此时其他core(如core n)立即读取最新数据(如更新为core 1中数据)。write-back(写回法)中非常有名的cache一致性算法MESI,它是典型的强一致CPU,intel就凭借MESI优雅地实现了强一致CPU,现在intel优化了下MESI,得到了MESIF,它有效减少了广播中req/rsp数量,减少了带宽占用,提高了处理器处理的吞吐量。关于MESI,这里有个可视化的MESI交互演示程序可以帮助理解其工作原理,查看MESI可视化交互程序。我们就先结合简单的MESI这个强一致性协议来试着理解下x86下为什么就可以保证强一致,结合多线程场景分析:一个volatile共享变量被多个线程读取,假定这几个线程跑在不同的cpu核心上,每个核心有自己的cache,线程1跑在core1上,线程2跑在core2上。现在线程1准备修改变量值,这个时候会先修改cache中的值然后稍后某个时刻写回主存或者被其他core读取。cache同步策略“write-back”,MESI就是其中的一种。处理器所有的读写操作都能被总线观测到,snoop based cache coherency,当线程2准备读取这个变量时:假定之前没读取过,发现自己的cache里面没有,就通过总线向内存请求,为了保证cpu cache高吞吐量,总线上所有的事务都能被其他core观测到,core1发现core2要读取内存值,这个数据刚好在我的cache里面,但是处于dirty状态。core1可能灰采取两种动作,一种是将dirty数据直接丢给core2(至少是最新的),或者告知core2延迟read,等我先写回主存,然后core2再尝试read内存。假定之前读取过了,core1对变量的修改也会被core2观测到,core1应该将其cache line标记为modified,将core2 cache line标记为invalidate使其失效,下次core2读取时从core1获取或内存获取(触发core1将dirty数据写回主存)。这么看来只要处理器的cache一致性算法支持,并且结合volatile避免寄存器相关优化,就能轻松保证线程可见行。但是不同的处理器设计不一样,我们只是以MESI协议来粗略了解了x86的处理方式,对于其他非强一致性CPU,即便使用了volatile也不一定能保证线程可见性,但若是对volatile变量读写时安插了类似MFENCE、LOCK指令也是可以的?如何进一步判断呢?还需要判断编译器(如gcc)是否有对volatile来做特殊处理,如安插MFENCE、LOCK指令之类的。上面编写的反汇编测试示例中,gcc生成的汇编没有看到lock相关的指令,但是因为我是在x86上测试的,而x86刚好是强一致CPU,我也不确定是不是因为这个原因,gcc直接图省事略掉了lock指令?所以现在要验证下,在其他非x86平台上,gcc -O2优化时做了何种处理。如果安插了类似指令,问题就解决了,我们也可以得出结论,c、c++中volatile在gcc处理下可以保证线程可见性,反之则不能得到这样的结论!我在网站godbolt.org交叉编译测试了一下上面gcc处理的代码,换了几个不同的硬件平台也没发现有生成特定的类似MFENCE或者LOCK相关的致使处理器cache失效后重新从内存加载的指令。备注:在某些处理器架构下,gcc确实有提供一些特殊的编译选项允许绕过CPU cache直接对内存进行读写,可参考gcc man手册“-mcache-volatile”、“-mcache-bypass”选项的描述。想了解下CC++中volatile的真实设计“意图”,然后,在stack overflow上我又找到了这样一个回答:https://stackoverflow.com/a/12878500,重点内容已加粗显示。[Nicol Bolas](https://stackoverflow.com/use…:What volatile tells the compiler is that it can’t optimize memory reads from that variable. However, CPU cores have different caches, and most memory writes do not immediately go out to main memory. They get stored in that core’s local cache, and may be written… eventually.**CPUs have ways to force cache lines out into memory and to synchronize memory access among different cores. These memory barriers allow two threads to communicate effectively. Merely reading from memory in one core that was written in another core isn’t enough; the core that wrote the memory needs to issue a barrier, and the core that’s reading it needs to have had that barrier complete before reading it to actually get the data.volatile guarantees none of this. Volatile works with “hardware, mapped memory and stuff” because the hardware that writes that memory makes sure that the cache issue is taken care of. If CPU cores issued a memory barrier after every write, you can basically kiss any hope of performance goodbye. So C++11 has specific language saying when constructs are required to issue a barrier.Dietmar Kühl回答中提到:The volatile keyword has nothing to do with concurrency in C++ at all! It is used to have the compiler prevented from making use of the previous value, i.e., the compiler will generate code accessing a volatile value every time is accessed in the code. The main purpose are things like memory mapped I/O. However, use of volatile has no affect on what the CPU does when reading normal memory: If the CPU has no reason to believe that the value changed in memory, e.g., because there is no synchronization directive, it can just use the value from its cache. To communicate between threads you need some synchronization, e.g., an std::atomic<T>, lock a std::mutex, etc.最后看了标准委员会对volatile的讨论:http://www.open-std.org/jtc1/…简而言之,就是CC++中当然也想提供java中volatile一样的线程可见性、阻止指令重排序,但是考虑到现有代码已经那么多了,突然改变volatile的语义,可能会导致现有代码的诸多问题,所以必须要再权衡一下,到底值不值得为volatile增加上述语义,当前C++标准委员会建议不改变volatile语义,而是通过新的std::atmoic等来支持上述语义。结合自己的实际操作、他人的回答以及CC++相关标准的描述,我认为CC++ volatile确实不能保证线程可见性。但是由于历史的原因、其他语言的影响、开发者自己的误解,这些共同导致开发者赋予了CC++ volatile很多本不属于它的能力,甚至大错特错,就连Linus Torvards也在内核文档中描述volatile时说,建议尽量用memory barrier替换掉volatile,他认为几乎所有可能出现volatile的地方都可能会潜藏着一个bug,并提醒开发者一定小心谨慎。6. 实践中如何操作开发者应该尽量编写可移植的代码,像x86这种强一致CPU,虽然结合volatile也可以保证线程可见性,但是既然提供了类似memory barrier()、std::atomic等更加靠谱的用法,为什么要编写这种兼顾volatile、x86特性的代码呢?开发者应该编写可维护的代码,对于这种容易引起开发者误会的代码、特性,应该尽量少用,这虽然不能说成是语言设计上的缺陷,但是确实也不能算是一个优势。凡事都没有绝对的,用不用volatile、怎么用volatile需要开发者自己权衡,本文的目的主要是想总结CC++ volatile的“能”与“不能”以及背后的原因。由于个人认识的局限性,难免会出现错误,也请大家指正。

January 7, 2019 · 4 min · jiezi

JAVA volatile 关键字

volatile 关键字能把 Java 变量标记成"被存储到主存中"。这表示每一次读取 volatile 变量都会访问计算机主存,而不是 CPU 缓存。每一次对 volatile 变量的写操作不仅会写到 CPU 缓存,还会刷新到主存中。实际上从 Java 5 开始,volatile 变量不仅会在读写操作时访问主存,他还被赋予了更多含义。变量的可见性问题Java volatile 关键字保证了线程对变量改动的可见性。举个例子,在多线程 (不使用 volatile) 环境中,每个线程会从主存中复制变量到 CPU 缓存 (以提高性能)。如果你有多个 CPU,不同线程也许会运行在不同的 CPU 上,并把主存中的变量复制到各自的 CPU 缓存中,像下图画的那样若果不使用 volatile 关键字,你无法保证 JVM 什么时候从主存中读变量到 CPU cache,或把变量从 CPU cache 写回主存。这会导致很多并发问题,我会在下面的小节中解释。想像一下这种情形,两个或多个线程同时访问一个共享对象,对象中包含一个用于计数的变量:public class SharedObject { public int counter = 0;}如果 Thread-1 会增加 counter 的值,而 Thread-1 和 Thread-2 会不时地读取 counter 变量。在这种情形中,如果变量 counter 没有被声明成 volatile,就无法保证 counter 的值何时会 (被 Thread-1) 从 CPU cache 写回到主存。结果导致 counter 在 CPU 缓存的值和主存中的不一致:Thread-2 无法读取到变量最新的值,因为 Thread-1 没有把更新后的值写回到主存中。这被称作 “可见性” 问题,即其他线程对某线程更新操作不可见。volatile 保证了变量的可见性volatile 关键字解决了变量的可见性问题。通过把变量 counter 声明为 volatile,任何对 counter 的写操作都会立即刷新到主存。同样的,所有对 counter 的读操作都会直接从主存中读取。public class SharedObject { public volatile int counter = 0;}还是上面的情形,声明 volatile 后,若 Thread-1 修改了 counter 则会立即刷新到主存中,Thread-2 从主存读取的 counter 是 Thread-1 更新后的值,保证了 Thread-2 对变量的可见性。volatile 完全可见性volatile 关键字的可见性生效范围会超出 volatile 变量本身,这种完全可见性表现为以下两个方面:如果 Thread-A 对 volatile 变量进行写操作,Thread-B 随后该 volatile 变量进行读操作,那么 (在 Thread-A 写 volatile 变量之前的) 所有对 Thread-A 可见的变量,也会 (在 Thread-B 读 volatile 变量之后) 对 Thread-B 可见。当 Thread-A 读一个 volatile 变量时,所有其他对 Thread-A 可见的变量也会重新从主存中读一遍。很抽象?让我们举例说明:public class MyClass { private int years; private int months private volatile int days; public int totalDays() { int total = this.days; total += months * 30; total += years * 365; return total; } public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days; }}上面的 update() 方法给三个变量赋值 (写操作),其中只有 days 是 volatile 变量。完全可见性在这的含义是,当对 days 进行写操作时,线程可见的其他变量 (在写 days 之前的变量) 都会一同回写到主存,也就是说变量 months 和 years 都会回写到主存。上面的 totalDays() 方法一开始就把 volatile 变量 days 读取到局部变量 total 中,当读取 days 时,变量 months 和 years (在读 days 之后的变量) 同样会从主存中读取。所以通过上面的代码,你能确保读到最新的 days, months 和 years。指令重排的困扰为了提高性能,JVM 和 CPU 会被允许对程序进行指令重排,只要重排的指令语义保持一致。举个例子:int a = 1;int b = 2;a++;b++;上述指令可能被重排成如下形式,语义跟先前保持一致:int a = 1;a++;int b = 2;b++;然而,当你使用了 volatile 变量时,指令重排有时候会产生一些困扰。让我们再看下面的例子:public class MyClass { private int years; private int months private volatile int days; public void update(int years, int months, int days){ this.years = years; this.months = months; this.days = days; }}update() 方法在写变量 days 时,对变量 years 和 months 的写操作同样会刷新到主存中。但如果 JVM 执行了指令重排会发生什么情况?就像下面这样:public void update(int years, int months, int days){ this.days = days; this.months = months; this.years = years;}当变量 days 发生改变时,months 和 years 仍然会回写到主存中。但这一次,days 的更新发生在写 months 和 years 之前,导致 months 和 years 的新值可能对其他线程不可见,使程序语义发生改变。对此 JVM 有现成的解决方法,我们会在下一小节讨论这个问题。volatile 的 Happen-before 机制为了解决指令重排带来的困扰,Java volatile 关键字在可见性的基础上提供了 happens-before 这种担保机制。happens-before 保证了如下方面:如果其他变量的读写操作原本发生在 volatile 变量写操作之前,他们不能被指令重排到 volatile 变量的写操作之后。注意,发生在 volatile 变量写操作之后的读写操作仍然可以被指令重排到 volatile 变量写操作之前。happen-after 重排到 (volatile 写操作) 之前是允许的,但 happen-before 重排到之后是不允许的。如果其他变量的读写操作原本发生在 volatile 变量读操作之后,他们不能被指令重排到 volatile 变量的读操作之前。注意,发生在 volatile 变量读操作之前的读操作仍然可以被指令重排到 volatile 变量读操作之后。happen-before 重排到 (volatile 读操作) 之后是允许的,但 happen-after 重排到之前是不允许的。happens-before 机制确保了 volatile 的完全可见性volatile 并不总是行得通虽然关键字 volatile 保证了对 volatile 变量的读写操作会直接访问主存,但在某些情况下把变量声明为 volatile 还不足够。回顾之前举过的例子 —— Thread-1 对共享变量 counter 进行写操作,声明 counter 为 volatile 并不足以保证 Thread-2 总是能读到最新的值。实际上,可能会有多个线程对同一个 volatile 变量进行写操作,也会把正确的新值写回到主存,只要这个新值不依赖旧值。但只要这个新值依赖旧值 (也就是说线程先会读取 volatile 变量,基于读取的值计算出一个新值,并把新值写回到 volatile 变量),volatile 关键字不再能够保证正确的可见性 (其他文章会把这称为原子性)。在多线程同时共享变量 counter 的情形下,volatile 关键字已不足以保证程序的并发性。设想一下:Thread-1 从主存中读取了变量 counter = 0 到 CPU 缓存中,进行加 1 操作但还没把更新后的值写回到主存。Thread-2 同一时间从主存中读取 counter (值仍为 0) 到他所在的 CPU 缓存中,同样进行加 1 操作,也没来得及回写到主存。情形如下图所示:Thread-1 和 Thread-2 现在处于不同步的状态。从语义上来说,counter 的值理应是 2,但变量 counter 在两个线程所在 CPU 缓存中的值却是 1,在主存中的值还是 0。即使线程都把 counter 回写到主存中,counter 更新成1,语义上依然是错的。(这种情况应该使用 synchronized 关键字保证线程同步)什么时候使用 volatile像之前的例子所说:如果有两个或多个线程同时对一个变量进行读写,使用 volatile 关键字是不够用的,因为对 volatile 变量的读写并不会阻塞其他线程对该变量的读写。你需要使用 synchronized 关键字保证读写操作的原子性,或者使用 java.util.concurrent 包下的原子类型代替 synchronized 代码块,例如:AtomicLong, AtomicReference 等。如果只有一个线程对变量进行读写操作,其他线程仅有读操作,这时使用 volatile 关键字就能保证每个线程都能读到变量的最新值,即保证了可见性。volatile 的性能volatile 变量的读写操作会导致对主存的直接读写,对主存的直接访问比访问 CPU 缓存开销更大。使用 volatile 变量一定程度上影响了指令重排,也会一定程度上影响性能。所以当迫切需要保证变量可见性的时候,你才会考虑使用 volatile。 ...

October 30, 2018 · 3 min · jiezi