BlazeMaple BlazeMaple
首页
  • 基础知识

    • Java的基本数据类型
    • Java中的常用类String
    • Java中的异常
    • Java中的注解
    • Java中的反射机制
    • Java中的泛型
    • Java为什么是值传递
  • 集合框架

    • Java集合核心知识总结
    • HashMap的7种遍历方式
    • 源码分析
  • Java新特性

    • Java8新特性
  • IO流

    • Java基础IO总结
    • Java IO中的设计模式
    • Java IO模型
    • IO多路复用详解
  • 并发编程

    • 并发编程基础总结
  • JVM

    • JVM基础总结
  • MySQL

    • MySQL核心知识小结
    • MySQL 45讲
  • Redis

    • Redis核心入门知识简记
  • Spring
  • SpringCloud Alibaba
  • 开发工具

    • Git详解
    • Maven详解
    • Docker详解
    • Linux常用命令
  • 在线工具

    • json (opens new window)
    • base64编解码 (opens new window)
    • 时间戳转换 (opens new window)
    • unicode转换 (opens new window)
    • 正则表达式 (opens new window)
    • md5加密 (opens new window)
    • 二维码 (opens new window)
    • 文本比对 (opens new window)
  • 学习资源

    • 计算机经典电子书PDF
    • hot120
GitHub (opens new window)
首页
  • 基础知识

    • Java的基本数据类型
    • Java中的常用类String
    • Java中的异常
    • Java中的注解
    • Java中的反射机制
    • Java中的泛型
    • Java为什么是值传递
  • 集合框架

    • Java集合核心知识总结
    • HashMap的7种遍历方式
    • 源码分析
  • Java新特性

    • Java8新特性
  • IO流

    • Java基础IO总结
    • Java IO中的设计模式
    • Java IO模型
    • IO多路复用详解
  • 并发编程

    • 并发编程基础总结
  • JVM

    • JVM基础总结
  • MySQL

    • MySQL核心知识小结
    • MySQL 45讲
  • Redis

    • Redis核心入门知识简记
  • Spring
  • SpringCloud Alibaba
  • 开发工具

    • Git详解
    • Maven详解
    • Docker详解
    • Linux常用命令
  • 在线工具

    • json (opens new window)
    • base64编解码 (opens new window)
    • 时间戳转换 (opens new window)
    • unicode转换 (opens new window)
    • 正则表达式 (opens new window)
    • md5加密 (opens new window)
    • 二维码 (opens new window)
    • 文本比对 (opens new window)
  • 学习资源

    • 计算机经典电子书PDF
    • hot120
GitHub (opens new window)
  • IO流

    • Java基础IO总结
    • Java IO中的设计模式
    • Java IO模型
    • IO多路复用详解
  • 并发编程

    • 并发编程基础总结
    • 深入理解volatile关键字
      • 如何保证变量的可见性
        • 基于JMM模型理解可见性的原理
        • volatile保证可见性代码示例
      • volatile可以禁止指令重排序
      • volatile无法保证原子性
        • 无法保证原子性代码示例
    • 深入理解synchronized关键字
  • JVM

    • JVM基础总结
  • Java进阶
  • 并发编程
BlazeMaple
2024-01-16
目录

深入理解volatile关键字

# 如何保证变量的可见性

# 基于JMM模型理解可见性的原理

从JMM提供的抽象模型上来说,它保证可见性的方式如下,假设我们有一个volatile共享变量a,值为1,线程1和线程2协作的操作如下

  1. 由于是volatile变量,所以线程1就不会对应将本地内存设置为无效,直接从主存中获取,并加载到主存中。
  2. 然后线程1将值修改为2,由于需要保证可见性,所以JMM会把这个变量写如主存中。
  3. 线程2读取,同样因为volatile修饰的原因,不走本地内存,直接从主存中获取,从而保证缓存一致性。

在这里插入图片描述

# volatile保证可见性代码示例

代码如下所示,分别给num变量加上volatile和删除volatile

public class VolatileModify {
    private volatile static int num = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while (num == 0) {
            }
            System.out.println("num已被修改为:1" );
        });


        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            num++;
            System.out.println("t2修改num为1" );
        });

        t1.start();
        t2.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

输出结果

/**
* 加volatile关键字
* t2修改num为1
* num已被修改为:1
*/
1
2
3
4
5

而不加volatile,则t1无法感知改变就会一直走CPU Cache中的值,导致死循环。

/**
* 不加volatile
*t2修改num为1
*/
1
2
3
4

# volatile可以禁止指令重排序

volatile不仅可以保证可见性,还可以避免指令重排序。如果我们将变量声明为 volatile ,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。

双重校验锁实现对象单例(线程安全):

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判断对象是否已经实例过,没有实例化过才进入加锁代码
        if (uniqueInstance == null) {
            //类对象加锁
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 1 执行了 1 和 3,此时 线程2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

在这里插入图片描述

# volatile无法保证原子性

volatile 关键字能保证变量的可见性,但不能保证对变量的操作是原子性的。

# 无法保证原子性代码示例

public class VolatoleAtomicityDemo {
    public volatile static int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService threadPool = Executors.newFixedThreadPool(5);
        VolatoleAtomicityDemo volatoleAtomicityDemo = new VolatoleAtomicityDemo();
        for (int i = 0; i < 5; i++) {
            threadPool.execute(() -> {
                for (int j = 0; j < 500; j++) {
                    volatoleAtomicityDemo.increase();
                }
            });
        }
        // 等待1.5秒,保证上面程序执行完成
        Thread.sleep(1500);
        System.out.println(inc);
        threadPool.shutdown();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

正常情况下,运行上面的代码理应输出 2500。但真正运行了上面的代码之后,每次输出结果都小于 2500。也就是说,如果 volatile 能保证 inc++ 操作的原子性的话。每个线程中对 inc 变量自增完之后,其他线程可以立即看到修改后的值。5 个线程分别进行了 500 次操作,那么最终 inc 的值应该是 5*500=2500

实际上,inc++ 其实是一个复合操作,包括三步:

  1. 读取 inc 的值。
  2. 对 inc 加 1。
  3. 将 inc 的值写回内存。

volatile 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现:

  1. 线程 1 对 inc 进行读取操作之后,还未对其进行修改。线程 2 又读取了 inc的值并对其进行修改(+1),再将inc 的值写回内存。
  2. 线程 2 操作完毕后,线程 1 对 inc的值进行修改(+1),再将inc 的值写回内存。

这也就导致两个线程分别对 inc 进行了一次自增操作后,inc 实际上只增加了 1。

其实,如果想要上面的代码运行正确,可以利用 synchronized、Lock或者AtomicInteger。

使用 synchronized 改进:

public synchronized void increase() {
    inc++;
}
1
2
3

使用 AtomicInteger 改进:

public AtomicInteger inc = new AtomicInteger();

public void increase() {
    inc.getAndIncrement();
}
1
2
3
4
5

使用 ReentrantLock 改进:

Lock lock = new ReentrantLock();
public void increase() {
    lock.lock();
    try {
        inc++;
    } finally {
        lock.unlock();
    }
}
1
2
3
4
5
6
7
8
9
帮助我们改善此页面! (opens new window)
上次更新: 2024/08/13, 09:07:12
并发编程基础总结
深入理解synchronized关键字

← 并发编程基础总结 深入理解synchronized关键字→

最近更新
01
SpringCloud Alibaba实战
08-22
02
SpringCloud Alibaba核心知识
08-22
03
两数之和
08-08
更多文章>
Theme by Vdoing | Copyright © 2023-2024 BlazeMaple
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式