随着互联的飞速发展,互联网公司也越来越高,并发多线程,内存管理,JVM调优等成为面试必问题。
volatile 是 Java 虚拟机提供的轻量级的同步机制。
他的三大特性:
在解释可见性之前需要先看一下什么是 JMM?
JMM(Java 内存模型 Java Memory Model,简称 JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM 关于同步的规定:
JMM 保证:可见性、原子性、有序性
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方成为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作空间,然后对变量进行操作,操作完成再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存储存着主内存中的变量副本拷贝,因此不同的线程无法访问对方的工作内存,此案成间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:
可见性:当一个线程修改主物理内存的值再写回主内存时,其他线程能都收到修改数据的通知。
如果看不懂上面的一段话,那么看看代码,然后再会过头看上面的文字。代码演示:
class MyData{
//第一次不加 ,第二次运行加 volatile
int n = 0;
public void change() {
this.n=60;
}
}
/**
* 1.验证 volatile 的可见性
* 2.假设 int n = 0; n 变量再之前根本没有添加volatile 关键字修饰,没有可见性
* @author taotao
*
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
// 生成线程修改 n
new Thread(()-> {
System.out.println(Thread.currentThread().getName()+" 进入程序");
try {
Thread.sleep(3000);//休眠3s
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.change();
System.out.println(Thread.currentThread().getName()+" 修改数据完成 "+myData.n);
},"线程1").start();
while(myData.n==0) {
//主线程等待
}
System.out.println(Thread.currentThread().getName()+" 完成工作,n 最新值" + myData.n);
}
}
第一次运行结果:
第二次运行结果:
操作不可分割,又叫完整性,既某个线程正在做某个具体业务,中间不可以被加塞或者被分割,需要整体完整。要不同时成功,要不同时失败。volatile 是不保证原子性的。
先来代码验证:
class MyData{
volatile int n = 0;
// 此时 n 已经加 volatile 关键字,但是 volatile 不保证原子性。
public void addPulsPuls() {
n++;
}
}
/* 验证 volatile 不保证原子性*/
public class VolatileDemo2 {
public static void main(String[] args) {
MyData myData = new MyData();
// 生成 20 个线程 ,同时修改 n 值
for (int i = 0; i < 20; i++) {
new Thread(()-> {
for (int j = 0; j < 1000; j++) {
myData.addPulsPuls();
}
},"线程"+i).start();
}
// Java 默认两个线程:main线程和 GC线程
while(Thread.activeCount()>2) {
Thread.yield(); //当前线程停止,让出资源
}
System.out.println(Thread.currentThread().getName()+" 完成工作,n 最新值" + myData.n);
}
}
第一次运行结果:
第二次运行结果:
根据代码大家可以知道正确的答案应该是1000*20=20000,其原因就是因为 volatile 不保证原子性。
虽然addPlusPuls执行的一行代码,但是使用 javap -c对class文件进行汇编的时候会发现,他的底层执行的是三行代码
附:JVM指令手册
执行 getfield 拿到原始 n
执行 iadd 进行加 1 操作
执行 putfield 写把累加后的值写回
那么,怎么解决这个问题?
这次我们使用 atomic 原子包下的 AtomicInteger 来解决这个问题
import java.util.concurrent.atomic.AtomicInteger;
class MyData{
//使用 AtomicInteger 的原子类
AtomicInteger ai = new AtomicInteger();
public void addAtomic() {
ai.getAndIncrement(); // 自增1
}
}
/* 解决 volatile 原子性的方法 */
public class VolatileDemo2 {
public static void main(String[] args) {
MyData myData = new MyData();
// 生成 20 个线程 ,同时修改 n 值
for (int i = 0; i < 20; i++) {
new Thread(()-> {
for (int j = 0; j < 1000; j++) {
myData.addAtomic();
}
},"线程"+i).start();
}
// Java 默认两个线程:main线程和 GC线程
while(Thread.activeCount()>2) {
Thread.yield(); //当前线程停止,让出资源
}
System.out.println(Thread.currentThread().getName()+" 完成工作,n 最新值" + myData.ai);
}
}
运行结果:
进阶:为什么 Atomic 能解决原子性的问题?
CAS。CAS 讲解
volatile 实现禁止指令重排序的优化,从而避免了多线程环境下程序出现乱序的现象
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个 CPU 指令,他的作用有两个:
由于编译器个处理器都能执行指令重排序优化,如果在指令间插入一条 Memory Barrier 则会告诉编译器和 CPU,不管什么指令都不能个这条 Memory Barrier 指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后执行重排序优化。内存屏障另一个作用是强制刷出各种 CPU 缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。
下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图:
下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图:
线程安全性保证
代码举例:
public void example(){
int x = 5; //语句1
int y = 10; //语句2
x = x + 5; //语句3
y = x + x; //语句4
}
观察 example 方法,方法体有四个语句,在多线程环境下,编译器会对这个四条语句执行的顺序进行调整,所以可能的执行顺序是:1->2->3->4;1->3->2>4;2->1->3->4 等。但是不会出现 语句4 在第一位,因为他的执行有依赖条件。正是因为在这种情况下,编译器会自动对代码执行的顺序进行一个优化调整,但是在多线程的情况下我们是希望他根据我们写好的顺序(1-2-3-4)依次执行,换句话说就是禁止编译器进行语句优化调整,也就是 volatile 的第三个特性“禁止指令重排”。