热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

volatile关键字作用及原理

volatile关键字作用及原理-当访问共享变量的多个线程运行在多核CPU上时,可能会出现可见性问题。synchronized关键字和lock可以解决这个问题,但是会阻塞线程,降低

当访问共享变量的多个线程运行在多核CPU上时,可能会出现可见性问题。synchronized关键字和lock可以解决这个问题,但是会阻塞线程,降低性能,所以java给出了更轻量级关键字volatile,不会阻塞线程。volatile有两个作用,一个是保证共享变量的可见性,另一个是防止指令重排序。

为理解volatile关键字的作用和原理,需要先了解一些计算机基础知识。

并发编程的三个特性

并发编程时,线程安全涉及三个特性:原子性、可见性、有序性。

原子性

要么全做,要么全部不做。
java中的原子操作包括:

  1. longdouble之外的基本类型的赋值操作
  2. 所有引用reference的赋值操作
  3. java.concurrent.Atomic.* 包中所有类的一切操作
    longdouble,因为它们在32位操作系统上,会被分成两部分进行更新,所以不是原子操作。
    自加操作(i++)不是原子操作,因为它是读取i的值赋值给局部变量tmptmp+1把结果赋值给i,3个原子操作的集合。

可见性

所有线程都能看到共享内存的最新状态。
多个线程访问同一个变量时,变量被一个线程修改后,能被其他线程看到。即多个线程访问同一变量时,看到的变量值是一致的。

有序性

为提高CPU流水线并行性能,编译器会对指令进行重排序,把没有被依赖的指令重新排序。

java 内存模型 (JMM)

程序存储在外存中,当进程被执行时,代码被调入主存。
但是主存的速度远远慢于CPU的速度,为提高性能,CPU中添加高速缓存cache,CPU不直接访问主存,而是通过cache和主存交互。当指令被执行时,指令所在块先从主存调入cache,CPU再访问cache获取指令。
CPU通常有多个内核,每个内核都有独立的cache。当多线程同时访问一块共享变量时,他们可能运行在不同的内核上,共享变量在每个内存的cache都有一个备份,线程各自操作自己所在内核cache中共享变量的备份,会造成缓存一致性问题。
Java虚拟机规范定义了一种java内存模型(JMM),来屏蔽掉各种硬件和操作系统的内存访问差异,让Java程序在各种平台上都能达到一致的内存访问效果。为了获得较好的执行性能,Java内存模型并没有限制执行引擎使用处理器的寄存器或者告诉缓存来提升指令执行速度,也没有限制编译器对指令进行重排序。简单的来说,在Java内存模型中,会存在缓存一致性和指令重排序的问题。

volatile 作用

volatile用于保证修饰变量的可见性、有序性,但是不能保证原子性。

共享变量可见性及实现原理

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,会从内存中重新读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,一方面此时内存中可能还是原来的旧值,因此无法保证可见性,另一方面可能访问的是线程自己所在内核cache中的缓存数据,而不是主存数据,即使主存中数据已更新,也无法读取到最新值。

可见性实现原理

  1. 线程1修改volatile修饰的变量时,会立即写入主存,并把主存当前块在其它cache中缓存都置为无效。
  2. 其他线程所在cache探听总线,当发现自己块对应的主存已修改时,就把本cache中对应块置为无效状态。当处理器要对这个数据读写时,发现块已失效,会重新从主存中调入块到cache后,再读取,那么读取到的数据就是最新的。
    所以,如果一个变量被volatile所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个volatile在并发编程中,其值在多个缓存中是可见的。

通过synchronizedLock也能够保证可见性,他们能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。但是这两种方式会阻塞其他线程执行,对性能损耗较大。

防止重排序及原理

代码在实际执行过程中,并不全是按照编写的顺序进行执行的,在保证单线程执行结果不变的情况下,编译器或者CPU可能会对指令进行重排序,以提高程序的执行效率。但是在多线程的情况下,指令重排序可能会造成一些问题,最常见的就是双重校验锁单例模式:
用懒加载方式实现单例模式时,通常使用双重检查加锁的方式(DCL),代码实现如下。

public class Singleton {
    public static volatile Singleton singleton;

    private Singleton() {};

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

通常的实现中没有使用到volatile关键字,那这里为什么要加上volatile修饰呢?要理解这个问题,先要了解对象的创建过程,实例化一个对象分为三个步骤:

  1. 分配内存空间
  2. 初始化对象
  3. 将内存空间的地址赋值给引用
    但由于编译器可以对指令进行重排序,所以上面的过程也可能变成如下过程:
  4. 分配内存空间
  5. 将内存空间的地址赋值给引用
  6. 初始化对象
    如果是这个过程,那么多线程环境下就将一个未初始化对象的引用暴露出来,从而导致不可预料的结果。
    因此,为了防止这个过程的重排序,需要将这个变量修改为volatile类型。

禁止重排序原理
volatile防止指令重排序是通过内存屏障来实现的。编译器在生成字节码文件时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
内存屏障分为如下三种:

  • Store Barrier: Store屏障,是x86的”sfence“指令,强制所有在store屏障指令之前的store指令,都在该store屏障指令执行之前被执行。
  • Load Barrier: Load屏障,是x86上的”ifence“指令,强制所有在load屏障指令之后的load指令,都在该load屏障指令执行之后被执行
  • Full Barrier: Full屏障,是x86上的”mfence“指令,复合了load和save屏障的功能。
    Java内存模型中volatile变量在写操作之后会插入一个store屏障,在读操作之前会插入一个load屏障,并且volatile修饰的变量的读写指令不能和其前后的任何指令重排序,其前后的指令可能会被重排序。一个类的final字段会在初始化后插入一个store屏障,来确保final字段在构造函数初始化完成并可被使用时可见。也正是JMM在volatile变量读写前后都插入了内存屏障指令,进而保证了指令的顺序执行。

不能完全保证原子性

volatile用于修饰简单类型变量时,如int、float、boolean,对它们的操作就会变成原子级别的。
但有一定的限制,如果volatile修改的简单变量与该变量以前的值相关,那么volatile不起作用,所以下方test()方法不是原子操作,如果要想使这种情况变成原子操作,需要使用synchronized关键字,如test2()方法。

class Test {
    volatile int n;

    private void test() {
        n++;
        n = n + 1;
    }

    private synchronized void test2() {
        n++;
        n = n + 1;
    }
}

所以使用volatile关键字时要慎重,并不是只要简单类型变量使用volatile修饰,对这个变量的所有操作都是原子操作,当变量的值由自身的上一个决定时,如n=n+1n++等,volatile关键字将失效,只有当变量的值和自身上一个值无关时对该变量的操作才是原子级别的,如n = m + 1,这个就是原子级别的。
另外,如果使用AtomicInteger.set(AtomicInteger.get() + 1),会和上述情况一样有并发问题,要使用AtomicInteger.getAndIncrement()才可以避免并发问题。

总结

  1. 正确的使用场景:一写多读,只由一个线程更新,其他线程都来读取。
  2. volatile是轻量级同步机制。在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,是一种比synchronized关键字更轻量级的同步机制。
  3. volatile只能保证内存可见性,不能保证原子性,所以不能替代synchronized和加锁机制。后两种机制既可以确保可见性又可以确保原子性。
  4. volatile不能修饰写入操作依赖当前值的变量。声明为volatile的简单变量如果当前值与该变量以前的值相关,那么volatile关键字不起作用,也就是说如下的表达式都不是原子操作:count++count = count+1
  5. 当要访问的变量已在synchronized代码块中,或者为常量时,没必要使用volatile
  6. volatile频繁从内存中读写,且屏蔽掉了JVM中必要的代码优化,和普通变量比较,效率上比较低,因此一定在必要时才使用此关键字。

参考

Java volatile的作用
volatile关键字及其作用
volatile的作用及正确的使用模式
[面试必备]深入理解Java的volatile关键字


推荐阅读
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • ejava,刘聪dejava
    本文目录一览:1、什么是Java?2、java ... [详细]
  • 线程漫谈——线程基础
    本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。进程与线程理解线程是至关重要的,每个进程至少有一个线程,进程是线程的容器,线程才是真正的执行体,线程必 ... [详细]
  • 开发笔记:python协程的理解
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了python协程的理解相关的知识,希望对你有一定的参考价值。一、介绍什么是并发?并发的本质就是 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 嵌入式处理器的架构与内核发展历程
    本文主要介绍了嵌入式处理器的架构与内核发展历程,包括不同架构的指令集的变化,以及内核的流水线和结构。通过对ARM架构的分析,可以更好地理解嵌入式处理器的架构与内核的关系。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 篇首语:本文由编程笔记#小编为大家整理,主要介绍了软件测试知识点之数据库压力测试方法小结相关的知识,希望对你有一定的参考价值。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • 深入理解线程、进程、多线程、线程池
    本文以QT的方式来走进线程池的应用、线程、进程、线程池、线程锁、互斥量、信号量、线程同步等的详解,一文让你小白变大神!为什么要使用多线程、线程锁、互斥量、信号量?为什么需要线程 ... [详细]
  • 第七课主要内容:多进程多线程FIFO,LIFO,优先队列线程局部变量进程与线程的选择线程池异步IO概念及twisted案例股票数据抓取 ... [详细]
  • Java编程思想一书中第21章并发中关于线程间协作的一节中有个关于汽车打蜡与抛光的小例子(原书的704页)。这个例子主要展示的是两个线程如何通过wait ... [详细]
  • vb.net不用多线程如何同时运行两个过程?不用多线程?即使用多线程,也不会是“同时”执行,题主只要略懂一些计算机编译原理就能明白了。不用多线程更不可能让两个过程同步执行了。不过可 ... [详细]
author-avatar
手机用户2602920263
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有