因为我说:volatile 是轻量级的 synchronized,面试官让我回去等通知!

volatile 是并发编程的重要组成部分,也是面试常被问到的问题之一。不要向小强那样,因为一句:volatile 是轻量级的 synchronized,而与期望已久的大厂失之交臂。

volatile 有两大特性:保证内存的可见性和禁止指令重排序。那什么是可见性和指令重排呢?接下来我们一起来看。

内存可见性

要了解内存可见性先要从 Java 内存模型(JMM)说起,在 Java 中所有的共享变量都在主内存中,每个线程都有自己的工作内存,为了提高线程的运行速度,每个线程的工作内存都会把主内存中的共享变量拷贝一份进行缓存,以此来提高运行效率,内存布局如下图所示:

但这样就会产生一个新的问题,如果某个线程修改了共享变量的值,其他线程不知道此值被修改了,就会发生两个线程值不一致的情况,我们用代码来演示一下这个问题。

public class VolatileExample {
    // 可见性参数
    private static boolean flag = false;

    public static void main(String[] args) {
		new Thread(() -> {
            try {
                // 暂停 0.5s 执行
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
            System.out.println("flag 被修改成 true");
        }).start();
        
        // 一直循环检测 flag=true
        while (true) {
            if (flag) {
                System.out.println("检测到 flag 变为 true");
                break;
            }
        }
    }
}

以上程序的执行结果如下:

flag 被修改成 true

我们会发现永远等不到 检测到 flag 变为 true 的结果,这是因为非主线程更改了 flag=true,但主线程一直不知道此值发生了改变,这就是内存不可见的问题。

内存的可见性是指线程修改了变量的值之后,其他线程能立即知道此值发生了改变。

我们可以使用 volatile 来修饰 flag,就可以保证内存的可见性,代码如下:

public class VolatileExample {
    // 可见性参数
    private static volatile boolean flag = false;

    public static void main(String[] args) {
		new Thread(() -> {
            try {
                // 暂停 0.5s 执行
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
            System.out.println("flag 被修改成 true");
        }).start();
        
        // 一直循环检测 flag=true
        while (true) {
            if (flag) {
                System.out.println("检测到 flag 变为 true");
                break;
            }
        }
    }
}

以上程序的执行结果如下:

检测到 flag 变为 true flag 被修改成 true

指令重排

指令重排是指在执行程序时,编译器和处理器常常会对指令进行重排序,已到达提高程序性能的目的。比如小强要去图书馆还上次借的书,随便再借一本新书,而此时室友小王也想让小强帮他还一本书,未发生指令重排的做法是,小强先把自己的事情办完,再去办室友的事,这样显然比较浪费时间,还有一种做法是,他先把自己的书和小王的书一起还掉,再给自己借一本新书,这就是指令重排的意义。

但指令重排不能保证指令执行的顺序,这就会造成新的问题,如下代码所示:

public class VolatileExample {
    // 指令重排参数
    private static int a = 0, b = 0;
    private static int x = 0, y = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            Thread t1 = new Thread(() -> {
                // 有可能发生指令重排,先 x=b 再 a=1
                a = 1;
                x = b;
            });
            Thread t2 = new Thread(() -> {
                // 有可能发生指令重排,先 y=a 再 b=1
                b = 1;
                y = a;
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            System.out.println("第 " + i + "次,x=" + x + " | y=" + y);
            if (x == 0 && y == 0) {
                // 发生了指令重排
                break;
            }
            // 初始化变量
            a = 0;
            b = 0;
            x = 0;
            y = 0;
        }
    }
}

以上程序执行结果如下所示:可以看出执行到 48526 次时发生了指令重排,y 就变成了非正确值 0,显然这不是我们想要的结果,这个时候就可以使用 volatile 来禁止指令重排。

以上我们通过代码的方式演示了指令重排和内存可见性的问题,接下来我们用代码来演示一下 volatile 同步方式的问题。

volatile 非同步方式

首先,我们使用 volatile 修饰一个整数变量,再启动两个线程分别执行同样次数的 ++ 和 -- 操作,最后发现执行的结果竟然不是 0,代码如下:

public class VolatileExample {
    public static volatile int count = 0; // 计数器
    public static final int size = 100000; // 循环测试次数

    public static void main(String[] args) {
        // ++ 方式
        Thread thread = new Thread(() -> {
            for (int i = 1; i <= size; i++) {
                count++;
            }
        });
        thread.start();
        // -- 方式
        for (int i = 1; i <= size; i++) {
            count--;
        }
        // 等所有线程执行完成
        while (thread.isAlive()) {}
        System.out.println(count); // 打印结果
    }
}

以上程序执行结果如下:

1065

可以看出,执行结果并不是我们期望的结果 0,我们把以上代码使用 synchronized 改造一下:

public class VolatileExample {
    public static int count = 0; // 计数器
    public static final int size = 100000; // 循环测试次数

    public static void main(String[] args) {
        // ++ 方式
        Thread thread = new Thread(() -> {
            for (int i = 1; i <= size; i++) {
                synchronized (VolatileExample.class) {
                    count++;
                }
            }
        });
        thread.start();
        // -- 方式
        for (int i = 1; i <= size; i++) {
            synchronized (VolatileExample.class) {
                count--;
            }
        }
        // 等所有线程执行完成
        while (thread.isAlive()) {}
        System.out.println(count); // 打印结果
    }
}

这次执行的结果变成了我们期望的值 0。

这说明 volatile 只是轻量级的线程可见方式,并不是轻量级的同步方式,所以并不能说 volatile 是轻量级的 synchronized,终于知道为什么面试官让我回去等通知了。

volatile 使用场景

既然 volatile 只能保证线程操作的可见方式,那它有什么用呢?volatile 在多读多写的情况下虽然一定会有问题,但如果是一写多读的话使用 volatile 就不会有任何问题。volatile 一写多读的经典使用示例就是 CopyOnWriteArrayList,CopyOnWriteArrayList 在操作的时候会把全部数据复制出来对写操作加锁,修改完之后再使用 setArray 方法把此数组赋值为更新后的值,使用 volatile 可以使读线程很快的告知到数组被修改,不会进行指令重排,操作完成后就可以对其他线程可见了,核心源码如下:

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
    private transient volatile Object[] array;
    
	final void setArray(Object[] a) {
        array = a;
    }	
    //...... 忽略其他代码
}

总结

本文我们通过代码的方式演示了 volatile 的两大特性,内存可见性和禁止指令重排,使用 ++ 和 -- 的方式演示了 volatile 并非轻量级的同步方式,以及 volatile 一写多读的经典使用案例 CopyOnWriteArrayList。

原创不易,期待你的素质三连,ღ( ´・ᴗ・` )比心~

1. 全栈架构之打包推荐【建议收藏,常读】

2. 一个空格引发的“惨案“

3. 分布式系统中Session共享的常用方案

4Java语言“坑爹”排行榜TOP 10

5. 我是一个Java类(附带精彩吐槽)

6. mysql索引失效,差点我的工作凉了

7. 既生synchronized,何生volatile?

8. 微服务一直火,为什么服务化要搞懂?

9. MySQL的COUNT语句,不简单!

10. 漫画:HashSet和TreeSet实现与原理

扫码二维码关注我

·end·

—如果本文有帮助,请分享到朋友圈吧—

我们一起愉快的玩耍!

你点的每个赞,我都认真当成了喜欢

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值