• Java并发编程:如何正确使用 volatile、synchronized 和 final 关键字


    Java并发编程专栏

    文章收录于Java并发编程专栏


    Java并发编程:如何正确使用 volatile、synchronized 和 final 关键字

      Java代码经编译器编译后,生成的指令顺序可能与源代码的顺序有别,且编译器会将变量存于寄存器而非内存中。CPU处理器采用乱序或并行的方式来执行指令,且在执行过程中保存在CPU处理器本地缓存中的值,对于其他处理器是不可见的。

      为了解决线程间的可见性和有序性问题,Java提供了volatile、synchronized 和 final 三个关键字,它们就像三个威力强大的法宝,助你轻松应对并发编程的挑战。

    synchronized:线程同步的守护神

      synchronized 关键字就像一位守护神,确保代码块在同一时刻只能被一个线程访问。可以理解成它是一把锁,锁住代码块,其他线程想要进入,就必须去争抢锁资源。这样就可以避免多个线程同时访问共享资源,造成数据不一致的问题。

      synchronized作用于某段代码块,可以保证可见性和有序性。从JVM底层实现来看,synchronized代码块是由一对儿monitorenter/monitorexit指令实现的,Monitor对象是同步的基本实现单元。

      在Java 6之前,Monitor的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。

      在Oracle JDK中,对JVM中的synchronized实现进行了很大的改进,增加了偏斜锁(Biased Locking)、轻量级锁和重量级锁,JVM检测到不同的竞争状况,就会采用锁的升级和降级来切换到最合适的锁实现,从而提高了性能。

    1. 初始化:在没有竞争出现的时候,会默认使用偏斜锁,JVM利用CAS(compare and swap)操作,在对象头上的Mark Word部分设置线程ID,以表示这个对象偏向于当前线程,所以并不涉及真正互斥锁。之所以这样做,是因为在大多数应用场景中,大部分对象生命周期中最多只会被一个线程锁定,使用偏斜锁可以降低锁开销。
    2. 锁升级:如果其它的线程试图锁定某个已经被偏斜锁标记的对象,JVM就会撤销偏斜锁并切换到轻量级锁。轻量级锁同样依赖CAS操作Mark Word来获取锁,如果切换轻量级锁失败,就会升级为重量级锁。
    3. 锁降级:当JVM进入安全点(SafePoint)的时候,会检查是否有闲置的Monitor,然后试图进行降级。

    volatile:轻量级的同步机制

      volatile关键字就像一位轻骑兵,它保证变量的更新操作对其他线程立即可见,但不会阻止其他线程对变量的读写操作。最原始的意义就是禁用CPU缓存和编译优化,所以volatile关键字可以保证可见性和有序性。volatile相比sychronized关键字来说,是相对轻量级的同步机制。

      volatile变量是Java语言提供的一种稍弱的同步机制,用来确保将变量的更新操作通知到其它线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量时共享的,因此不会将该变量上的操作进行与其它内存操作一起重排序。

      简言之,volatile通过禁用 CPU 缓存和编译优化,以确保变量值始终是最新的。

    int x = 2int y = 0;
    volatile int i = 0;
    int x = 3;
    int y = 1;
    

      以上代码中“i”变量使用volatile修饰,那么对这个变量的读写不能使用CPU缓存,必须从内存中读取或者写入。并且根据Happens-Before规则中的volatile规则,“i”变量前两个语句不会排到“i”变量后面,“i”变量后两个语句不会排在i变量前面。

      虽然volatile可以在一定程度上保存可见性,但是不能滥用(能不用就不用)。只有当满足特定条件时才可使用,例如变量的每次赋值都是独立的,只有一个线程更新变量,访问变量不需要加锁等。

    final:变量的永恒承诺

      final关键字与volatile关键字正好相反,final修饰变量时会告诉编译器,这个变量生而不变可以使劲优化。final 定义类型及说明如下:

    • final class:不允许被继承。
    • final 方法:不允许 Override。
    • final 局部变量:不允许修改。
    • final 实例属性:只能赋值一次;安全发布。
    • final static 属性:只能赋值一次,编译期就放入方法区,不允许修改。

    synchronized和volatile的区别是什么?

    • volatile 本质是在告诉 JVM 当前变量在寄存器中的值是不确定的,需要从主存中读取;synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程的访问会被阻塞。
    • volatile 仅能使用在变量级别;synchronized 则可以使用在变量、方法和类上。
    • volatile 仅能保证可见性,不能保证原子性;而 synchronized 则可以保证可见性和原子性。
    • volatile 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。
    • volatile 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。

    小结

      选择合适的同步机制需要根据具体场景进行判断。如果需要保证原子性和可见性,并且允许多个线程同时访问共享资源,则可以使用 volatile。如果需要保证原子性、可见性和有序性,并且只允许一个线程访问共享资源,则可以使用 synchronized。

      当然,也可以将 volatile 和 synchronized 结合使用,例如,使用 volatile 保证变量的可见性,使用 synchronized 保证变量的原子性和有序性。

      综上,volatile 关键字可以保证可见性和有序性,synchronized 关键字可以保证可见性和有序性,同时还可以保证原子性,final 关键字可以保证变量的不可变。在实际开发中,需要根据具体的需求选择合适的关键字来保证程序的正确性和性能。


    一键三连,让我的信心像气球一样膨胀!

  • 相关阅读:
    Taurus.MVC 微服务框架 入门开发教程:项目集成:5、统一的日志管理。
    Pytorch遇到的坑:MSEloss的输入问题
    关于Python和自动化
    AngelScript -- C++程序最好的脚本语言
    VUE:组件
    第3章 动态SQL
    前后端分离项目跨域请求的问题与解决办法
    DDE图像增强
    【软件与系统安全笔记】七、模糊测试
    MySQL事务
  • 原文地址:https://blog.csdn.net/Jas000/article/details/139456039