Java并发编程系列:volatile关键字
一、原子性、可见性和有序性
Java内存模型主要是围绕着线程并发过程中如何处理原子性、可见性和顺序性这三个特征来设计的。
1、原子性
原子性表示任意时刻只有一个线程可以执行某一段功能代码,以防止多个线程同时访问某些共享数据时,造成错误。
2、可见性
可见性是指一个线程修改了某个共享变量后,其他线程能够立刻访问到被修改后的最新数据,也就是共享数据对其他线程都是可见的。
volatile修饰的变量在修改后会立即同步到主内存,在使用时会重新从主内存中读取。volatile变量是依赖主内存为中介来保证多线程下变量对其他线程的可见性。
synchronized关键字是通过在unlock之前必须把变量同步回主内存来实现可见性的。
final关键字则是因为变量在初始化后,值就不会更改,所以只要在初始化过程中没有把this指针传递出去也能保证对其他线程的可见性。
3、有序性
程序在运行时,指令的执行顺序并不是严格按照从上到下顺序执行的,可能会进行指令重排。根据CPU流水线作业,一般来说,简单的操作会先执行,复杂的操作后执行。
有序性从不同的角度来看是不同的。单纯从单线程来看都是有序的,但到了多线程就不一样了。可以这么理解,如果在一个线程内部观察,所有操作都是有序的。但是如果在一个线程内观察另一个线程,操作可能是无序的。也就是CPU进行的指令重排序对单线程程序而言,不会有什么问题,但是对于多线程程序,就可能出现问题。而有序性就是防止指令重排序带来的问题。
二、Java内存模型
1、内存模型产生原因
Java存在一个线程可见性的问题,是由于Java内存模型的原因。
Java是跨平台语言,可以支持不同的硬件平台,这是Java虚拟机的功劳。Java内存模型(Java Memory Model,JMM)是Java虚拟机规范定义的,用来屏蔽掉Java程序在各种不同硬件和操作系统对内存访问的差异,这样就可以实现Java程序在各种不同的硬件平台上都能达到内存访问的一致性。可以避免像c、c++等直接使用物理硬件和操作系统的内存模型在不同操作系统和硬件平台下的不兼容。比如有些c/c++程序在windows平台运行正常,而在linux平台运行就会出问题。
2、内存模型概念
虽然Java程序运行在Java虚拟机上面,内存等是虚拟机的一部分,但实际也是使用的物理机的,只不过是Java虚拟机屏蔽了底层硬件细节,统一做了处理。
Java内存模型的主要目标是定义程序中变量的访问规则,即在Java虚拟机中将变量存储到主内存或将变量从主内存中取出这样的底层细节。需要注意的是这里的变量跟Java程序中的变量不是完全等同的。这里的变量是指实例字段,静态字段,构成数组对象的元素,但是不包括线程内部的局部变量和方法参数,因为这是线程私有的。
这里可以简单的认为,主内存是Java虚拟机内存区域中的堆,局部变量和方法参数是在虚拟机栈中定义的。但是在堆中的变量如果在多线程中使用,就涉及到了堆和不同虚拟机栈中变量的值的一致性问题了。
Java内存模型概念:
主内存:Java虚拟机规定所有的变量都必须在主内存中产生,为了方便理解,可以认为是堆区。与前面说的物理机的主内存相比,物理机的主内存是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。
工作内存:Java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的,为了方便理解,可以认为是虚拟机栈。线程的工作内存保存了线程需要的变量在主内存中的副本。
Java虚拟机规定,线程对主内存共享变量的操作必须在线程的各自的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,必须通过主内存来作为中介进行传递。这里需要说明一下,Java内存模型是一个抽象概念,其实并不存在,它描述的是一种规范。类似下图
三、volatile和synchronized关键字
1、volatile
volatile关键字修饰的变量可以保证可见性和有序性。volatile类型的变量在修改后会立即同步到主内存,在使用的时候会从主内存中重新读取,是依赖主内存为中介来保证多线程下变量对其他线程的可见性的。而volatile禁止了指令重排序,从而保证指令的有序性。
线程访问volatile变量时,能够获取到最新值,但volatile变量并不能用于线程同步。
2、volatile和synchronized的区别
synchronized用于线程同步,但是和volatile不一样,synchronized可以保证原子性、可见性和有序性。
四、volatile使用场景
1、状态标识
比如用一个变量作为状态标识来控制多个线程协同工作。每个线程通过判断状态标识的值来确定是否执行相关代码。那这个状态标识的变量就可以使用volatile关键字修饰,以确保当某个线程更新了变量的值之后,其他的线程可以立即获得最新的值。
2、一个线程写,多个线程读
volatile很适用一个线程写,多个线程读的场合。比如,类似发布订阅的机制,当一个写线程更新volatile变量值的话,其他读线程可以立即获取到最新值。
3、volatile和synchronized实现“低开销读-写锁”
如下显示的线程安全的计数器,使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。
public class CheesyCounter { private volatile int value; //读操作,无需synchronized,提高性能 public int getValue() { return value; } //写操作,必须synchronized。因为x++不是原子操作 public synchronized int increment() { return value++; } }
参考文章:
https://www.jianshu.com/p/5584600d2569
https://www.jianshu.com/p/15106e9c4bf3