volatile关键字

提示:阅读这篇文章的时候最好先掌握Java内存模型(JMM)的相关内容,不然可能会感到不适。

大多数人接触到这个关键字都是在学习单例模式的时候,他可以保证在并发的场景下不会产生多个实例对象的情况。

通常volatile用来修饰成员变量的时候,

  1. 可以保证该成员变量在不同线程之间的可见性
  2. 可以防止编译器和处理器对该成员变量进行重新排序,保证有序性
  3. 无法保证该成员变量的原子性,并发场景下线程不安全。

1 可见性

我们用两个线程来模拟一下,不同线程的工作内存之间的可见性。

public class VolatileDemo {
    public static int INIT = 0;
    public static int MAX = 6;

    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        // 线程1
        pool.execute(() -> {
            int v = INIT;
            while (v < MAX) {
                if (v != INIT) {
                    System.out.println(Thread.currentThread().getName() + " >>> 获取更新后的值:" + INIT);
                    v = INIT;
                }
            }
        });
        // 线程2
        pool.execute(() -> {
            int v = INIT;
            while (INIT < MAX) {
                System.out.println(Thread.currentThread().getName() + " >>> 将值更新为:" + ++v);
                INIT = v;
                try {
                    TimeUnit.MILLISECONDS.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

先简单说明一下,线程1始终运行,检查自身工作内存中INIT的值是否有变化。线程2会修改自身工作内存中的INIT值,并同步到主内存中。所以,程序的输出应该是:

pool-1-thread-2 >>> 将值更新为:1
pool-1-thread-1 >>> 获取更新后的值:1
pool-1-thread-2 >>> 将值更新为:2
pool-1-thread-2 >>> 将值更新为:3
pool-1-thread-2 >>> 将值更新为:4
pool-1-thread-2 >>> 将值更新为:5
pool-1-thread-2 >>> 将值更新为:6

所以,线程2的工作内存中的INIT对于线程1来说是不可见的,线程1无法感知到线程2对于INIT的修改。

如果对INIT使用volatile关键字,那么任何线程修改了INIT,都要立即将他写回到主内存中,并且这会使其他线程中的INIT数据失效,想要继续使用他的时候,都必须要从主内存中重新获取

INIT加上volatile关键字后并运行输出

public volatile static int INIT = 0;
pool-1-thread-2 >>> 将值更新为:1
pool-1-thread-1 >>> 获取更新后的值:1
pool-1-thread-2 >>> 将值更新为:2
pool-1-thread-1 >>> 获取更新后的值:2
pool-1-thread-2 >>> 将值更新为:3
pool-1-thread-1 >>> 获取更新后的值:3
pool-1-thread-2 >>> 将值更新为:4
pool-1-thread-1 >>> 获取更新后的值:4
pool-1-thread-2 >>> 将值更新为:5
pool-1-thread-1 >>> 获取更新后的值:5
pool-1-thread-2 >>> 将值更新为:6
pool-1-thread-1 >>> 获取更新后的值:6

可以看到,线程1时刻都感知到了INIT值的变化。

注意

有一种说法是volatile修饰对象或数组的时候,针对的是引用,数组或对象中的成员变量不具备可见性。

我在做这种测试的时候并没有产生这种结果,我在做如下示例的时候volatile依然对对象中的成员变量产生影响了。不知道是我的代码有问题还是这种说法是错误的。

public class VolatileDemo {
    public static InitObj initObj = new InitObj(0);
    public static int MAX = 6;

    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        // 线程1
        pool.execute(() -> {
            int v = initObj.getInit();
            while (v < MAX) {
                if (v != initObj.getInit()) {
                    System.out.println(Thread.currentThread().getName() + " >>> 获取更新后的值:" + initObj.getInit());
                    v = initObj.getInit();
                }
            }
        });
        // 线程2
        pool.execute(() -> {
            int v = initObj.getInit();
            while (initObj.getInit() < MAX) {
                System.out.println(Thread.currentThread().getName() + " >>> 将值更新为:" + ++v);
                initObj.setInit(v);
                try {
                    TimeUnit.MILLISECONDS.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    @Data
    static
    class InitObj {
        private int init;

        public InitObj(int i) {
            this.init = i;
        }
    }
}

2 有序性

验证有序性有个很好的例子是单例模式:

public class LazySingleton {
    private static volatile LazySingleton instance = null;

    private LazySingleton() {}

    public static LazySingleton getInstance() {
        if (instance == null) {
            synchronized (LazySingleton.class) {
                if (instance == null) {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}

上面是双重检查锁机制的单例模式,我们都知道synchronized关键字可以保证有序性,那么为什么还要加上volatile关键字呢?

原因就是,两者保证有序性的方式不一样。synchronized无法禁止指令重排,被synchronized包裹的代码块就算发生指令重排,由于同一时间内只有一个线程执行逻辑,所以就算是指令重排也可以保证有序性。

而volatile则是使用内存屏障的方式禁止指令重排,从而保证有序性。内存屏障是个很底层的概念大概的作用就是重排序时不能把后面的指令重排序到内存屏障之前。

我们看一下上述代码的部分字节码

Code:
      stack=2, locals=2, args_size=0
         0: getstatic     #2                  // Field instance:Lcom/spheign/szjx/designModel/singleton/LazySingleton;
         3: ifnonnull     37
         6: ldc           #3                  // class com/spheign/szjx/designModel/singleton/LazySingleton
         8: dup
         9: astore_0
        10: monitorenter
        11: getstatic     #2                  // Field instance:Lcom/spheign/szjx/designModel/singleton/LazySingleton;
        14: ifnonnull     27
        17: new           #3                  // class com/spheign/szjx/designModel/singleton/LazySingleton
        20: dup
        21: invokespecial #4                  // Method "<init>":()V
        24: putstatic     #2                  // Field instance:Lcom/spheign/szjx/designModel/singleton/LazySingleton;
        27: aload_0
        28: monitorexit
        29: goto          37
        32: astore_1
        33: aload_0
        34: monitorexit
        35: aload_1
        36: athrow
        37: getstatic     #2                  // Field instance:Lcom/spheign/szjx/designModel/singleton/LazySingleton;
        40: areturn

其中17、20、21、24为instance = new LazySingleton();的主要操作。我们也可以拆分成下面三个步骤:

  1. 分配存储LazySingleton对象的内存空间;
  2. 初始化LazySingleton对象;
  3. 将instance指向刚刚分配的内存空间。

以上是正常的顺序,但是编译器为了优化程序的性能,有可能的执行顺序是:

  1. 分配存储LazySingleton对象的内存空间;
  2. 将instance指向刚刚分配的内存空间;
  3. 初始化LazySingleton对象。

这时问题就来了,线程1先进来执行,并且已经将instance指向LazySingleton对象的内存空间,但还没有初始化LazySingleton对象。与此同时,线程2执行到了判断if (instance == null),由于instance指向LazySingleton对象的内存空间,所以判断false,直接返回instance对象。这样线程2使用instance对象的时候就会发生空指针异常。

3 原子性

我们先来看一个例子:

public class VolatileDemo {
    public volatile int i = 0;

    public void increase() {
        i++;
    }

    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            pool.execute(() -> {
                for (int j = 0; j < 100; j++) {
                    volatileDemo.increase();
                }
            });
        }
        pool.shutdown();
        System.out.println(volatileDemo.i);
    }
}

如果你用的IDE是idea的话,那么你会发现一个提示

原子性提示

这是由于i++不是一个原子性的操作,我们拆分一下就是两步操作,先执行i+1,再将i+1赋值给i

所以程序的运行结果肯定不会是我们预期的1000,而是小于1000的某个值。稍加分析我们就能理解为什么是这种结果,线程1和线程2都同时获取了i的值,比如是10,线程1执行了+1操作,将11写回到主内存中,线程2工作内存中的i将失效。在线程1向主内存中回写数据之前线程2也完成了+1的操作,所以就算是工作内存中的i失效了也不会影响线程2再将11写回到主内存中。

解决办法有两个,加锁或者使用atomic类。

synchroinzed:

下例中也可以不加volatile关键字

public class VolatileDemo {
    public volatile int i = 0;
    private final Object lock = new Object();
    public void increase() {
        synchronized (lock){
            i++;
        }

    }

    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            pool.execute(() -> {
                for (int j = 0; j < 100; j++) {
                    volatileDemo.increase();
                }
            });
        }
        pool.shutdown();
        System.out.println(volatileDemo.i);
    }
}

AtomicInteger:

public class VolatileDemo {
    public AtomicInteger i = new AtomicInteger(0);
    public void increase() {
            i.incrementAndGet();
    }

    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            pool.execute(() -> {
                for (int j = 0; j < 100; j++) {
                    volatileDemo.increase();
                }
            });
        }
        pool.shutdown();
        System.out.println(volatileDemo.i);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,874评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,102评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,676评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,911评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,937评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,935评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,860评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,660评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,113评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,363评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,506评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,238评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,861评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,486评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,674评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,513评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,426评论 2 352