volatile关键字分析

之前,我对volatile的理解是很浅显的,实际操作时就出错了。今天来深入了解一下volatile这个关键字。

一、volatile的两个语义

  • volatile关键字有两个语义:
    • 保证可见性
    • 禁止指令重排序优化
  • 可见性:指的就是在多线程环境中,如果一个线程修改了某个变量的值,新值对于其他线程来说是可以立即得知的。我们知道,Java内存模型是通过在变量修改后将新值同步回主内存,在遍历读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,而volatile修饰的变量和普通变量不同的地方就是,volatile可以使新值能立即同步回主内存,以及每次使用前立即从主内存刷新,因此volatile保证了多线程操作时变量的可见性。
  • 禁止指令重排序:volatile为什么能禁止指令重排序呢?我们需要知道虚拟机是怎样做的了,这时候从字节码层面上分析就没意义了,所以需要看JIT编译后的汇编代码。

没有在windows下获取过JIT编译后的汇编代码的同学可以看我记录的步骤,会看汇编代码的同学可以略过下面这一部分。

Windows没有HSDIS插件的同学需要先下载这个插件:下载链接

解压后放到%JAVA_HOME%\jre\bin\server里

接着运行以下命令就可以看到汇编代码了

java -server -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:CompileCommand=compileonly,*Test.getInstance Test

比如在DCL单例代码中:

public class Test {

    private static Test instance;

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

    public static void main(String[] args) {
        getInstance();
    }
}

其中,对instance赋值部分的编译后的代码如下:

  0x0000000002cd455c: mov     rax,0dafdd6a8h    ;   {oop(a 'java/lang/Class' = 'Test')}
  0x0000000002cd4566: mov     rsi,qword ptr [rsp+20h]
  0x0000000002cd456b: mov     r10,rsi
  0x0000000002cd456e: mov     dword ptr [rax+68h],r10d
  0x0000000002cd4572: shr     rax,9h
  0x0000000002cd4576: mov     rsi,11b0a000h
  0x0000000002cd4580: mov     byte ptr [rax+rsi],0h
  0x0000000002cd4584: lock add dword ptr [rsp],0h  ;*putstatic instance
                                                ; - Test::getInstance@24 (line 12)

有volatile修饰时,赋值后多了一个”lock addl $0x0,(%esp)”操作,这个操作可以形成一个内存屏障,有了内存屏障,在指令重排时就不能把屏障后的指令重排到前面。

lock前缀的指令的作用就是引起当前CPU的Cache写入内存,并且该动作会使其他CPU的Cache无效,这样就可以让volatile变量的修改对其他CPU立即可见。

听起来这像是在保证可见性是吧?其实是这样的,在执行”lock addl $0x0,(%esp)”这个操作时,修改已经都同步到了主内存,这就意味着在之前的操作都已经被执行完成了,所以便形成“指令重排序无法越过内存屏障”的效果。

二、volatile不能保证原子性

面试时被问到,能用volatile实现一个计数器吗?答案是不能的。解释如下:

首先我们看这个计数器的具体例子:

public class Test {

    private volatile static int count = 0;

    private static void inc() {
        count++;
    }

    public static void main(String[] args) {
        // 开10个线程
        for (int i = 0; i < 10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    // 每个线程执行1000次自增
                    for (int j = 0; j < 1000; j++)
                    Test.inc();
                }
            }).start();
        }
        // 让前面线程执行完
        while(Thread.activeCount()>1)
            Thread.yield();
        System.out.println(count);
    }
}

我们创建了1000个线程去执行这个inc()方法,运行后我们发现,结果不是1000,这是为什么呢?我们明明用volatile来修饰count变量了,volatile可以保证可见性,在inc()方法中一个线程进行count++ 得到的值其他线程应该能立即看到才对呀,然后1000个线程分别都对这个count进行了自增,最后应该得到1000。这就是使用volatile时常见的一个误区,这段程序错在了没有保证原子性,为什么没有保证原子性呢?注意了,这是因为自增操作(count++)不是一个原子操作,也就是说count++是由多条字节码指令构成的,大概是三个子操作:读、修改、写,而这三个子操作有可能被分割开执行,也就是说,可能一个线程在读数据,这时volatile可以保证它读的正确性,但是volatile不能保证原子性,所以有可能同时有另一个线程在修改或者写操作,那么之前那个线程在读的数据就失效了,所以最后的putstatic指令可能会把错误的值同步回主内存,

那么,这段代码怎么改才能是线程安全的呢?

有以下几种方法:

  1. 使用synchronized

synchronized会将inc()方法变成一个同步块,当有线程在里面时,其他线程会被阻塞

    private synchronized static void inc() {
        count++;
    }
  1. 使用java.util.concurrent.locks里的lock锁

通过获取锁,释放锁来确保线程安全

    private static Lock lock = new ReentrantLock();

    private static void inc() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
  1. 使用AtomicInteger

这个类可以保证用原子方式更新的 int 值

    private static AtomicInteger count = new AtomicInteger();

    private static void inc() {
            count.getAndIncrement();
    }
内容概要:本文档详细介绍了在三台CentOS 7服务器(IP地址分别为192.168.0.157、192.168.0.158和192.168.0.159)上安装和配置Hadoop、Flink及其他大数据组件(如Hive、MySQL、Sqoop、Kafka、Zookeeper、HBase、Spark、Scala)的具体步骤。首先,文档说明了环境准备,包括配置主机名映射、SSH免密登录、JDK安装等。接着,详细描述了Hadoop集群的安装配置,包括SSH免密登录、JDK配置、Hadoop环境变量设置、HDFS和YARN配置文件修改、集群启动与测试。随后,依次介绍了MySQL、Hive、Sqoop、Kafka、Zookeeper、HBase、Spark、Scala和Flink的安装配置过程,包括解压、环境变量配置、配置文件修改、服务启动等关键步骤。最后,文档提供了每个组件的基本测试方法,确保安装成功。 适合人群:具备一定Linux基础和大数据组件基础知识的运维人员、大数据开发工程师以及系统管理员。 使用场景及目标:①为大数据平台建提供详细的安装指南,确保各组件能够顺利安装和配置;②帮助技术人员快速掌握Hadoop、Flink等大数据组件的安装与配置,提升工作效率;③适用于企业级大数据平台的建与维护,确保集群稳定运行。 其他说明:本文档不仅提供了详细的安装步骤,还涵盖了常见的配置项解释和故障排查建议。建议读者在安装过程中仔细阅读每一步骤,并根据实际情况调整配置参数。此外,文档中的命令和配置文件路径均为示例,实际操作时需根据具体环境进行适当修改。
在无线通信领域,天线阵列设计对于信号传播方向和覆盖范围的优化至关重要。本题要求设计一个广播电台的天线布局,形成特定的水平面波瓣图,即在东北方向实现最大辐射强度,在正东到正北的90°范围内辐射衰减最小且无零点;而在其余270°范围内允许出现零点,且正西和西南方向必须为零。为此,设计了一个由4个铅垂铁塔组成的阵列,各铁塔上的电流幅度相等,相位关系可自由调整,几何布置和间距不受限制。设计过程如下: 第一步:构建初级波瓣图 选取南北方向上的两个点源,间距为0.2λ(λ为电磁波波长),形成一个端射阵。通过调整相位差,使正南方向的辐射为零,计算得到初始相位差δ=252°。为了满足西南方向零辐射的要求,整体相位再偏移45°,得到初级波瓣图的表达式为E1=cos(36°cos(φ+45°)+126°)。 第二步:构建次级波瓣图 再选取一个点源位于正北方向,另一个点源位于西南方向,间距为0.4λ。调整相位差使西南方向的辐射为零,计算得到相位差δ=280°。同样整体偏移45°,得到次级波瓣图的表达式为E2=cos(72°cos(φ+45°)+140°)。 最终组合: 将初级波瓣图E1和次级波瓣图E2相乘,得到总阵的波瓣图E=E1×E2=cos(36°cos(φ+45°)+126°)×cos(72°cos(φ+45°)+140°)。通过编程实现计算并绘制波瓣图,可以看到三个阶段的波瓣图分别对应初级波瓣、次级波瓣和总波瓣,最终得到满足广播电台需求的总波瓣图。实验代码使用MATLAB编写,利用polar函数在极坐标下绘制波瓣图,并通过subplot分块显示不同阶段的波瓣图。这种设计方法体现了天线阵列设计的基本原理,即通过调整天线间的相对位置和相位关系,控制电磁波的辐射方向和强度,以满足特定的覆盖需求。这种设计在雷达、卫星通信和移动通信基站等无线通信系统中得到了广泛应用。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值