【JAVA基础-多线程】- 深入理解volatile关键字

  • 并发编程的三个概念
  • Java内存模型JMM
  • volatile实战例子(原子性,有序性,可见性)

并发编程的三个概念

首先我们了解下并发编程三个重要的概念:原子性问题,可见性问题,有序性问题。我们先看具体看一下这三个概念:

原子性

即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性

即程序执行的顺序按照代码的先后顺序执行。(可能发生指令重排导致代码执行先后顺序发生变化)

Java 内存模型

Java内存模型规定所有的变量都是存在主存当中每个线程都有自己的工作内存线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

Java中的原子性

在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行。

Java中的可见性

对于可见性,Java提供了volatile关键字来保证可见性。
当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性

有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

volatile 实战

volatile关键字修饰的成员变量具有两大特性:

  • 保证了该成员变量在不同线程之间的可见性
  • 禁止对该成员变量进行重排序,也就保证了其有序性
  • 但是volatile修饰的成员变量并不具有原子性,在并发下对它的修改是线程不安全的。

1. 可见性 实战

通过对JMM的了解,我们都知道线程对主内存中共享变量的修改首先会从主内存获取值的拷贝,然后保存到线程的工作内存中。接着在工作内存中对值进行修改,最终刷回主内存。由于不同线程拥有各自的工作内存,所以它们对某个共享变量值的修改在没有刷回主内存的时候只对自己可见

举个例子,假如有两个线程,其中一个线程用于修改共享变量value,另一个线程用于获取修改后的value:

public class VolatileTest {

    private static int INT_VALUE = 0;
    private final static int LIMIT = 5;

    public static void main(String[] args) {

        new Thread(() -> {
            int value = INT_VALUE;
            while (value < LIMIT) {
                if (value != INT_VALUE) {
                    System.out.println("获取更新后的值:" + INT_VALUE);
                    value = INT_VALUE;
                }
            }
        }, "reader").start();

        new Thread(() -> {
            int value = INT_VALUE;
            while (value < LIMIT) {
                System.out.println("将值更新为: " + ++value);
                INT_VALUE = value;
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "writer").start();

    }
}

执行结果:
image.png

由于在不同的线程中,线程writer对INT_VALUE的改变并不知情,所以线程reader中INT_VALUE的值固定,只会执行一次System.out.println

修改上面的例子,将INIT_VALUE成员变量使用volatile关键字修饰:

public class VolatileTest {

    private volatile static int INT_VALUE = 0;
    private final static int LIMIT = 5;

    public static void main(String[] args) {

        new Thread(() -> {
            int value = INT_VALUE;
            while (value < LIMIT) {
                if (value != INT_VALUE) {
                    System.out.println("获取更新后的值:" + INT_VALUE);
                    value = INT_VALUE;
                }
            }
        }, "reader").start();

        new Thread(() -> {
            int value = INT_VALUE;
            while (value < LIMIT) {
                System.out.println("将值更新为: " + ++value);
                INT_VALUE = value;
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "writer").start();

    }
}

执行结果:
image.png

2. 有序性 实战

来看一个线程不安全的单例实现(双检索)

/**
 * 双检索(DCL式).
 */
public class Singleton03 {

    private static Singleton03 singleton03;

    // 私有构造方法
    private Singleton03() {
    }

    // 同步该方法获取单例对象
    public static Singleton03 getInstance() {
        // 但对象为空时同步这个方法
        if (singleton03 == null) {
            synchronized (Singleton03.class) {
                // 再判断是否为空
                if (singleton03 == null) {
                    // 为空的话就创建对象
                    singleton03 = new Singleton03();
                }
            }
        }
        return singleton03;
    }
}

上面的例子虽然加了同步锁,但是在多线程下并不是线程安全的。instance = new SingletonTest()在实际执行的时候会被拆分为以下三个步骤:

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

通过JMM的学习我们都知道,在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,因为第2步和第3步并没有依赖关系,所以可能发生重排序,排序后的步骤为:

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

经过重排序后,上面的例子在多线程下就会出现问题。假如现在有两个线程A和B同时调用SingletonTest#getInstance,线程A执行到了代码的instance = new SingletonTest(),已经完成了对象内存空间的分配并将instance指向了该内存空间,线程B执行到了if (instance == null) ,发现instance并不是null(因为已经指向了内存空间),所以就直接返回instance了。但是线程A并还没有执行初始化SingletonTest操作,所以实际线程B拿到的SingletonTest实例是空的,那么线程B后续对SingletonTest操控将抛出空指针异常。

要让上面的例子是线程安全的,只需要用volatile修饰单例对象即可:

public class SingletonTest {
    // 私有化构造方法,让外部无法通过new来创建对象
    private SingletonTest() {
    }

    // 单例对象
    private volatile static SingletonTest instance = null;

    // 静态工厂方法
    public static SingletonTest getInstance() {
        if (instance == null) { // 双重检索
            synchronized (SingletonTest.class) { // 同步锁
                instance = new SingletonTest();
            }
        }
        return instance;
    }
}

因为通过volatile修饰的成员变量会添加内存屏障来阻止JVM进行指令重排优化。

3. 原子性 线程不安全性 实战

举个递增的例子:

public class VolatileTest2 {

    private static int value = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> IntStream.range(0, 500).forEach(i -> value++));
        Thread thread2 = new Thread(() -> IntStream.range(0, 500).forEach(i -> value++));

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println(value);
    }
}

运行结果并不是所想象的,带又随机性,很可能小于1000;

volatile可以保证修改的值能够马上更新到主内存,其他线程也会捕捉到被修改后的值,那么为什么不能保证原子性呢?

因为在Java中,只有对基本类型的赋值和修改才是原子性的,而对共享变量的修改并不是原子性的。通过JMM内存交互协议我们可以知道,一个线程修改共享变量的值需要经过下面这些步骤:

  1. 线程从主内存中读取(read)共享变量的值,然后载入(load)到线程的工作内存中的变量;
  2. 使用(use)工作内存变量的值,执行加减操作,然后将修改后的值赋值(assign)给工作内存中的变量;
  3. 将工作内存中修改后的变量的值存储(store)到主内存中,并执行写入(write)操作。

所以上面的例子中,可能出现下面这种情况:

thread1和thread2同时获取了value的值,比如为100。thread1执行了+1操作,然后写回主内存,这个时候thread2刚好执行完use操作(+1),准备执行assign(将+1后的值写回工作内存对应的变量中)操作。虽然这时候thread2工作内存中value值的拷贝无效了(因为volatile的特性),但是thread2已经执行完+1操作了,它并不需要再从主内存中获取value的值,所以thread2可以顺利地将+1后的值赋值给工作内存中的变量,然后刷回主存。这就是为什么上面的累加结果可能会小于1000的原因。

要让上面的例子是线程安全的话可以加同步锁,或者使用atomic类。

public class VolatileTest2 {

    private volatile static int value = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> IntStream.range(0, 500).forEach(i -> {
            synchronized (VolatileTest2.class) {
                value++;
            }
        }));
        Thread thread2 = new Thread(() -> IntStream.range(0, 500).forEach(i -> {
            synchronized (VolatileTest.class) {
                value++;
            }
        }));

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        System.out.println(value);
    }
}

执行结果:1000

参考文献:
https://www.cnblogs.com/guanghe/p/9206635.html
https://mrbird.cc/volatile.html

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

推荐阅读更多精彩内容