【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://developer.aliyun.com/article/1631540
出自【进步*于辰的博客】启发博文:《Java volatile关键字最全总结:原理剖析与实例讲解(简单易懂)》(转发)。
参考笔记二,P73、P74.1;笔记三,P61。
在学习此关键字之前,我们先了解一下JMM规范和并发编程中的三个概念。
1、JMM规范
JMM(Java module memory,Java内存模型)是一个抽象概念,并不真实存在于内存。它是用于定义程序中各个变量(成员变量、类变量、数组元素等)的一组规范和规则,指定变量的访问方式。
规定:
- 线程解锁之前必须将共享变量刷新回主内存。
- 线程加锁之前必须读取主内存中变量的最新值到工作空间。
- 解锁和加锁必须是同一把锁。
大家可能不解其意,这就需要涉及另一个概念:线程空间.。
什么是线程空间?程序执行JMM规范的实体是线程,当线程创建时,JMM会为其创建一个私有内存(也称为工作内存、本地内存或栈空间)。JMM规定所有变量都保存在主内存,线程访问变量时需为变量创建一个副本至工作内存进行操作,完成后将变量值返回主内存,且线程通信在主内存进行。
注意:JMM作为抽象概念,规定中的“主内存”与“私有内存”等概念同样是抽象概念,并不一定真实对应CPU中的缓存和物理内存。
2、并发编程的三个概念
1:可见性
可见性指线程对变量的修改,其他线程可见,即由volatile修饰的变量,其私有内存失效。(具体说明见下文)
2:原子性
原子性指线程对变量的操作的整个过程不会被阻塞或分割。
原子性表示“拒绝多线程并发操作”,即同一时刻只能有一个线程进行操作。因此,在整个操作过程中不会被线程调度中断。
如:a = 1
是原子操作,而a++
不是,因为它分为读取、计算和赋值三个步骤。
在Java中,原子操作包括:
- 基本数据类型的读取与赋值,但限于将值赋给变量,变量间赋值不是原子操作。
- 引用赋值。
java.util.concurrent.atomic
包中所有类的一切操作。
3:有序性
有序性也称为“指令重排”,指程序运行时,编译器基于提高性能需要,以指令间的数据依赖性作为依据对指令进行重新排列。执行顺序:编译器重排 → 指令并行重排 → 内存系统重排
。
单线程环境下,无论指令如何重排,结果都不变,但多线程时可能会出现问题。
3、volatile
3.1 介绍
volatile是一种轻量级的同步机制,与synchronized有一些通性,但synchronized属于重量级(“级”是指对变量访问的限制程度)。
volatile遵循JMM规范实现了可见性和有序性,但不保证原子性。因此,限制线程在访问由volatile修饰的变量时,从主内存获取数据,而不是从工作内存。在数据操作完成后再刷新回主内存,故在保证原子性的情况下,线程安全。
如何保证原子性?
两种方法:
- 程序中不存在多线程对变量进行非原子性操作。
- 见下文。
3.2 volatile原理
在JVM底层,volatile是采用“内存屏障”来实现的。在所生成的汇编代码中可见,在volatile前多出了一条Lock前缀指令,这相当于内存屏障(也称为“内存栅栏”),其提供三项功能: - 屏蔽指令重排。
- 强制私有内存的修改立即写入主内存。
- 执行写操作时,致使CPU中其他线程的私有内存无效。
这就是为何volatile可实现可见性和有序性,但不保证原子性的原因。
3.3 如何保证原子性?
大家先看个示例。
static volatile int x = 0;
private static void add() {
x++;
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
int i = 100000;
while (i-- > 0) {
add();
}
});
Thread t2 = new Thread(() -> {
int i = 100000;
while (i-- > 0) {
add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(x);
}
请问最后的x
是200000
吗?绝大概率不是,因为volatile不能保证原子性,而x++
又不是原子操作,可谓必然会出现并发问题。
那么,volatile如何保证原子性?从上文【volatile原理】可得,volatile本身是无法保证原子性的,故需要采取其他方案。如下:
1:synchronized。
static int x = 0;
private synchronized static void add() {
x++;
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
int i = 100000;
while (i-- > 0) {
add();
}
});
Thread t2 = new Thread(() -> {
int i = 100000;
while (i-- > 0) {
add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(x);
}
2:Lock。
static int x = 0;
// Lock的原理类似synchronized
static Lock lock = new ReentrantLock();
private static void add() {
lock.lock();// 可视为内存屏障”,当然实际不是
x++;
lock.unlock();
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
int i = 100000;
while (i-- > 0) {
add();
}
});
Thread t2 = new Thread(() -> {
int i = 100000;
while (i-- > 0) {
add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(x);
}
3:AtomicInteger。
// 原子操作类是通过CAS循环的方式来保证原子性
static AtomicInteger x = new AtomicInteger();
private static void add() {
x.getAndIncrement();
}
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
int i = 100000;
while (i-- > 0) {
add();
}
});
Thread t2 = new Thread(() -> {
int i = 100000;
while (i-- > 0) {
add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(x);
}
4、volatile的一个经典运用
从文章《[Java]单例模式》中截取这段代码:
public static Singleton newInstance() {
if (instance == null) {
--------------------A
synchronized (Singleton.class) {
-------B
if (instance == null) {
------------C
instance = new Singleton();----D
}
}
}
return instance;---------------------------E
}
DCL,可解决“懒汉式”存在的线程安全问题。不过,仍有不足,问题在于D。
因为,实例化分为三步:
- 创建实例,分配内存。
- 实例初始化。
- 令
instance
指向此实例。
其中,2和3都依赖于1,而2与3之间没有依赖关系,故指令重排可能会将2与3调换。
调换有什么后果?假设一种情况,线程x调用newInstance()
,执行D,但还未进行实例初始化(已执行了1、3),此时线程y调用newInstance()
,判断A为false,直接执行E,此时返回的instance
未初始化,导致异常。
异常出现的原因就是指令重排,用volatile禁止指令重排即可解决(用volatile修饰instance
)。
最后
本文中的例子,是为了阐述volatile关键字和方便大家理解而简单举出的,不一定有实用性,仅是抛砖引玉。
本文完结。