并发编程学习之volatile关键字

目录

一、简介

二、volatile使用场景 - 保证可见性

三、volatile使用场景 - 禁止指令重排序

四、volatile使用场景 - 不保证原子性

五、总结


一、简介

volatile,是Java中的一个关键字。被volatile修饰的变量,在多个线程中保持可见性,注意,volatile不保证原子性,这也是volatile与synchronized的区别之一。

volatile关键字只保证可见性,不能保证原子性。

  • 什么是可见性?

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

在了解volatile的工作原理之前,首先需要了解一下Java内存模型,如下图:

在Java中,每个线程都有一个独立的内存空间,称为工作内存。 它保存了用于执行操作的不同变量的值。在执行操作之后,线程将变量的更新值复制到主内存中,这样其他线程可以从主内存中读取最新值。

二、volatile使用场景 - 保证可见性

如果需要保证某个变量在多个线程之间可见性的时候,可以使用volatile关键字进行修饰。我们来看一个例子:

public class T03_Volatile {
    /**
     * 如果没有加volatile修饰,线程A由于死循环,可能没有及时从主内存读取最新的running值
     * 加了volatile修饰,一旦running的值发生变化,就会通知其他线程需要从主内存重新获取值
     */
    private volatile boolean running = true;

    private void m1() {
        while (running) {
            System.out.println("hello....");
        }
    }

    public static void main(String[] args) {
        T03_Volatile t03_volatile = new T03_Volatile();
        new Thread(t03_volatile::m1, "A").start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t03_volatile.running = false;
    }
}

注意:上述程序有可能会出现在没加volatile关键字的情况下,线程A也能及时读取到最新的running值,这主要是由于CPU可能暂时空闲,自动从主内存同步了最新的running到线程A中,导致线程A执行结束。

三、volatile使用场景 - 禁止指令重排序

指令重排序,简单理解就是说,保证代码按照我们写的顺序执行。被volatile修饰了的变量,禁止了指令进行重排序,所以可以保证代码完全按照我们编写的顺序执行(不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的)。

比较典型的例子就是双重检查机制的单例模式,简称DCL单例,具体代码如下:

高频面试题:DCL(Double Check Lock)双重检查单例模式,为什么需要加volatile关键字?主要考点其实就是volatile禁止指令重排。

public class T03_Volatile {
    /**
     * 加入volatile关键字修饰
     */
    private volatile static T03_Volatile instance = null;

    private T03_Volatile() {
    }

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

注意我们创建实例的语句 instance = new T03_Volatile(),实际上底层分为三个步骤:

  • 1、分配对象的内存空间;
  • 2、初始化对象;
  • 3、设置实例对象指向刚分配的内存地址;

分析:

步骤2【初始化对象】需要依赖于步骤1【分配对象的内存空间】,但是步骤3【设置实例对象指向刚分配的内存地址】不需要依赖步骤2【初始化对象】,所以有可能出现1,3,2的执行顺序,当出现这种顺序的时候,虽然instance不为空,但是此时对象有可能没有正确初始化,直接拿来使用的话可能会报错。

四、volatile使用场景 - 不保证原子性

volatile关键字不能保证原子性。也就是说,对volatile修饰的变量进行的操作,不保证多线程安全。我们看一个案例:

public class T03_Volatile {
    private static CountDownLatch countDownLatch = new CountDownLatch(500);
    private volatile static int num = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.MICROSECONDS.sleep(400);
                    num++;
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("num = " + num);
    }
}

运行结果:多运行几次,发现结果不固定,有时候会是500,有时候是495,有时候是497,这就证明了volatile不保证原子性。

原因解析:

num++操作并不是原子操作,实际上num++的操作分成了三个步骤进行:

//获取变量的值
int temp = num;
//将该变量的值+1
num = num + 1;
//将该变量的值写回到对应的主内存中
num = temp;

举一个例子:

假设线程A首次拿到的num = 3,在执行+1操作前,可能存在其他多个线程已经对num做了修改,假设此时主内存中的num已经被修改到20了,而此时线程A执行+1操作,将num=3+1=4的结果又重新写回到了主内存中,将原本num应该是num = 20 + 1 = 21的,被线程A回写之后覆盖成了4,因此总的结果小于或者等于500,这就存在了原子性问题。

五、总结

本篇文章介绍了volatile关键字的基本用法,以及通过案例讲解了volatile在内存可见性、指令重排序方面的作用。总结一句话:

volatile关键字保证内存可见性,能禁止指令重排序,但是注意它不保证原子性,所以volatile不能完全替代synchronized关键字,因为synchronized保证原子性的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值