Java 并发理论基础
[TOC]
带着BAT大厂问题去理解
带着这些问题去继续后文,会很大程度的帮助你理解并发理论基础
- 多线程的出现是要解决什么问题?
- 线程不安全是指什么?举例说明
- 并发出现线程不安全的本质是什么?可见性?原子性和有序性。
- java 是怎么解决问题的?3 个关键字,JVM 和 8 个 Happens-Before
- 线程安全是不是非真即假?不是
- 线程安全有哪些实现思路?
- 如何理解并发和并行的区别?
为什么需要多线程
众所周知,CPU、内存、I/O 设备的速度是有极大差异的,为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
- CPU 增加了缓存,以均衡与内存的速度差异。//导致了
可见性问题 - 操作系统增加了进程、线程、以分时复用 cpu 资源,进而均衡 CPU 和 I/O 设备的速度差异。//导致了
原子性问题 - 编程程序优化指令执行次序,使得缓存能够得到更加合理的利用。//导致了
有序性问题(指令重排序)
线程不安全示例
如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结构是不一致的。
代码演示:
public class JucTest {
public static void main(String[] args) throws InterruptedException {
final int threadSize = 1000;
ThreadUnsafeExample threadUnsafeExample = new ThreadUnsafeExample();
// 使用 CountDownLatch 保证线程同时触发
CountDownLatch countDownLatch = new CountDownLatch(threadSize);
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < threadSize; i++) {
executorService.execute(()->{
threadUnsafeExample.add();
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(threadUnsafeExample.getCount());//执行结果可能为 1000,大部分情况下,线程并发的不一致,结果总是小于 1000
}
}
public class ThreadUnsafeExample {
private int count = 0;
public void add() {
count++;
}
public int getCount() {
return count;
}
}并发出现问题的根源:并发三要素
上述代码为什么输出结果不是 1000?并发出现问题的根源是什么?
可见性:cpu 缓存引起
可见性(Visibility):一个线程对共享变量的修改,能否被其他线程“及时”看到。
举个简单的例子,看下面的代码:
例子 1:
class MyTask {
boolean running = true;
void run() {
while (running) {
// do something
}
}
void stop() {
running = false;
}
}如果
run()和stop()在不同线程中执行,主线程调用
stop()后把running设为false,但是另一个线程在循环中,可能看不到这个更新,导致它永远不会退出循环
例子 2:
//线程 1 执行代码
int i = 0;
i = 10;
//线程 2 执行的代码
j = i;假若执行的 1 的是 cpu1,执行线程 2 的cpu2,由上面的分析可知,当线程 1 执行 i=10 这句时,会先把 i的初始值加载到 cpu1 的高速缓存,然后赋值为 10,此时 i 的值 10 只存在与 cpu1 的高速缓存,没有立刻写入到主存中去。
此时线程 2 执行 j = i,它会先去主存读取 i 的值并加载到cpu2 的缓存当中,注意此时内存当中的值还是 0 ,那么就会使得 J 的值为 0,而不是 10。
这就是可见性问题,线程 1 对变量 i 修改了之后,线程 2 没有立刻看到线程 1 修改的值。
原子性:分时复用引起
原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
举个最简单的例子,看下下面这段代码:
int i = 1;
//线程 1 执行
i += 1;
//线程 2 执行
i += 1;这里需要注意的是: i+=1 需要 3 条指令执行。
- 将变量 i 从内存中加载到 cpu 寄存器中。
- 在 cpu 寄存器中执行 i+1操作
- 将最后的结果 i 写入内存,(缓存机制导致可能写入的是 cpu 缓存而不是内存)。
由于 cpu 分时复用(线程切换)的存在,线程 1 执行了第一条指令后,就切换到线程 2 来执行,假如线程 2 执行完这 3 条指令后,再切换为线程 1 执行后续两条指令,将造成最后写入内存的结果是 2 而不是 3。
有序性:重排序引起
有序性:程序中代码的执行顺序,是否和你写的顺序一致。
int i = 0;
boolean flag = false;
i = 1; //语句 1
flag = true; // 语句 2上面代码定义了一个 int 型变量,定义了一个 boolean 型变量,然后分别对这两个变量进行赋值操作,语句 1 是在语句 2 前面的,那么 JVM 在真正执行这段代码的时候会保证语句 1 一定会在语句 2 前面执行吗?答案是不一定,为什么呢?因为这里可能会发生指令重排序。
在执行程序的时候,为了提高性能,编译器和处理器通常会对指令做重排序,重排序分为三种类型:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(ILP)来将多条指令重叠执行,如果不存在数据依赖性,处理器可以改变语句对应的机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序在执行。
从 java 源代码到最终实际执行的指令序列,会分别经历下面的三种重排序:
上述的 1 属于编译器的重排序,2 和 3 属于处理器重排序。这些重排序都可能导致多线程程序出现问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的重排序会要求 java 编译器在生成指令序列是,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序。
java 是怎么解决并发问题的:JMM(java 内存模型)
理解的第一个维度:核心知识点
JMM 本质上可以理解为,java内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体包括:
- volatile,synchronized 和 final 三个关键字
- Happens-Before 规则
理解的第二个维度:可见性,有序性,原子性
原子性
在 java 中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行
javax = 10;//直接将 x赋值为 10,也就是说这个语句执行会直接将 10 写入到工作内存中 y = x;//看着是一个简单的变量赋值,包含两个操作,先去读取 x 的值,再将 x 的值写入到工作内存(写 y),虽然读取 x 的值和写入 x 的值这两个操作都是原子的,但是两个操作合起来并不一定是原子的。 x++; //x++包含 3 个操作,读取 x 的值,进行+1 操作,写入新的值 x = x+1;// 等同于 x++;上述的操作语句,只有第一行的语句具有原子性。也就是说,只有简单的读取、赋值(而且是必须将数字赋值给某个变量,变量之间的相互赋值不是原子操作)才是原子操作。
从上面可以看出,java 内存模型只保证了基本读取和赋值是原子操作,如果要实现更大范围的操作原子性,可以通过 synchronized 和 Lock 来实现。由于 synchronized 和 Lock 能够保证只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。
可见性
java 提供了 volatile 关键字来保证可见性。
当一个共享变量被 volatile 修饰时,它会保证修改的值会立刻被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。
另外如果使用了 synchronized 和 Lock 也可以保证可见性,因为 synchronized 和 Lock 保证了同一个时刻只有一个线程获取锁执行同步代码快,并且在释放锁之前会将变量的修改结果刷新到主存中去。
有序性
在 java 里面,可以通过 volatile来保证一定的"有序性"。另外可以通过 synchronized 和 Lock 来保证有序性,原理同上也是保证某个时刻只有一个线程在执行同步代码块,相当于是让线程顺序执行代码,同一个锁对象的锁释放和锁获取之间具备 happens-before 关系。JMM 中通过 happens-before 来保证有序性。
关键字:volatile、synchronized 和 final
volatile 详解
引言
- volatile 关键字的作用是什么?
- volatile 能保证原子性吗?
- 之前32 位机上的共享的 long 和 double 变量为什么要用 volatile?现在 64 位机器上是否也要设置?
- i++为什么不能保证原子性?
- volatile 是如何实现可见性的?内存屏障?
- volatile 是如何实现有序性的?Happens-Before?
- volatile 的应用场景?
Volatile 作用详解
防重排序
我们从一个最经典的例子来分析重排序问题,大家应该都熟悉单例模式的实现,而在并发环境下的单例实现方式,我们通常可以使用双重检查锁(DCL)的方式来实现。其源码如下:
public class LazySingleton {
private static volatile LazySingleton lazySingleton;
//单例模式 构造方法私有
private LazySingleton() {
if (lazySingleton != null) {
throw new RuntimeException("无法再创建其他实例对象");
}
}
//该方法存在线程问题,多线程下可能获取到不同的对象
/*public static LazySingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
return lazySingleton;
}*/
//使用synchronized 关键字在方法上加锁,每次调用都会竞争锁,效率不高
//public synchronized static LazySingleton getInstance() {
// if (lazySingleton == null) {
// lazySingleton = new LazySingleton();
// }
// return lazySingleton;
//}
//在方法内部加锁,只有在lazySingleton 为空的情况下才会去竞争锁,减少锁竞争,提升性能
public static LazySingleton getInstance() {
// 此时的判断可能有执行重排序问题,
/*在多线程环境下,假设其中一个线程a获取到下方的锁后,由于指令重排序,执行顺序变为了
* 1. 分配内存空间
* 2.将对象指向分配的内存地址
* 3.初始化对象
* 线程a 2 步骤执行完成,线程b 进来执行下方判断,由于已经分配了内存地址,null 不成立,将直接返回未初始化后的对象,
* 如果此时对象被调用,就可能出错,所以需要用到 volatile 关键字*/
if (lazySingleton == null) {//第一次检查
synchronized (LazySingleton.class) {
if (lazySingleton == null) {//第二次检查
lazySingleton = new LazySingleton();
//正常的指令执行流程
//1. 分配内存空间
//2. 初始化对象
//3. 将对象指向分配的内存地址
return lazySingleton;
}
}
}
return lazySingleton;
}
}现在我们分析一下,为什么要在LazySingleton 上加上 volatile 关键字。要理解这个问题,先要了解对象的构造过程,实例化一个对象其实可以分为三个步骤:
- 分配内存空间
- 初始化对象
- 将内存空间地址赋值给对象的引用
但是由于操作系统可以 对指令进行重排序,所以上面的过程也可能变成如下过程:
- 分配内存空间
- 将内存空间的地址赋值给对象的引用
- 初始化对象
如果是这个流程,多线程环境下就可以将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为 volatile 类型的变量。(通过内存屏障来实现防止指令重排序,通过内存屏障,保证步骤 2 在步骤 3 之前执行)
实现可见性
可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。volatile 可以有效的解决这个问题。如下面的例子:
public class VolatileTest {
private static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread("Thead A") {
public void run() {
while (!stop) {
}
System.out.println(Thread.currentThread() + "stop");
}
};
// thread.setDaemon(true);//新建的线程默认为用户线程,非守护线程
thread.start();
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread() + "after 1 second");
}catch (InterruptedException e){
e.printStackTrace();
}
stop = true;
}
}执行输出如下:
Thread[main,5,main]after 1 second
//Thred A 一直在 loop,因为可见性原因没有看到 Thread Main已经修改了 stop 的值可以看到 Thread-main 休眠1秒之后,设置 stop = ture,但是Thread A根本没停下来,这就是可见性问题。如果通过在stop变量前面加上volatile关键字则会真正stop:
Thread[main,5,main]after 1 second
Thread[Thead A,5,main]stop
Disconnected from the target VM, address: '127.0.0.1:50717', transport: 'socket'
Process finished with exit code 0保证原子性:单词读/写
volatile不能保证完全的原子性,只能保证单次的读/写操作具有原子性。先从如下两个问题来理解
问题 1:i++为什么不能保证原子性
对于原子性,需要强调一点,也是大家容易误解的一点:对 volatile 变量的单词读/写操作可以保证原子性的,如 long 和 double 类型变量,但是不能保证 i++这种操作的原子性,因为本质上 i++是读、写两次操作。
现在我们就通过下列程序来演示写这个问题:
package com.hongsipeng.juc;
import java.util.concurrent.CountDownLatch;
/**
* @author hongsipeng
* @apiNote i++不能保证原子性测试
* @since 2025/5/28
*/
public class VolatileTest01 {
volatile int n;
public void add() {
n++;
}
public static void main(String[] args) throws InterruptedException {
final VolatileTest01 volatileTest01 = new VolatileTest01();
CountDownLatch countDownLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
try {
Thread.sleep( 10);
volatileTest01.add();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();//等待所有线程全部执行完成
System.out.println(volatileTest01.n);
//执行输出的结果不是 1000,说明 volatile 是不保证原子性的;
}
}大家可能会觉得对变量n 加上关键字之后,这段程序就是线程安全的,其实不然,从上述程序的运行结果可以看出,volatile 是无法保证原子性的(否则结果应该是 1000)。原因也很简单,i++是一个复合操作,包括 三个步骤:
- 读取 i 的 值
- 对i 加 1
- 将 i 的值写回内存。volatile 是无法保证这个三个操作是具有原子性的,我们可以通过 AtomicInteger或者 Synchronized 来保证+1 操作的原子性。
问题 2:共享的 long 和 double 变量的为什么要用 volatile?
因为 long 和 double 两种数量类型的操作可分为高 32 位和低 32 位两部分,因此普通的 long 或 double 类型读/写可能不是原子的。因此,鼓励大家将共享的 long 和 double 变量设置为 volatile 类型,这样能保证任何情况下对 long 和 double 的单词读/写操作都是原子性
如下是 JLS 中的解释:
17.7 Non-Atomic Treatment of double and long
For the purposes of the Java programming language memory model, a single write to a non-volatile long or double value is treated as two separate writes: one to each 32-bit half. This can result in a situation where a thread sees the first 32 bits of a 64-bit value from one write, and the second 32 bits from another write.
Writes and reads of volatile long and double values are always atomic.
Writes to and reads of references are always atomic, regardless of whether they are implemented as 32-bit or 64-bit values.
Some implementations may find it convenient to divide a single write action on a 64-bit long or double value into two write actions on adjacent 32-bit values. For efficiency’s sake, this behavior is implementation-specific; an implementation of the Java Virtual Machine is free to perform writes to long and double values atomically or in two parts.
Implementations of the Java Virtual Machine are encouraged to avoid splitting 64-bit values where possible. Programmers are encouraged to declare shared 64-bit values as volatile or synchronize their programs correctly to avoid possible complications.
目前,各种平台上的商用虚拟机都选择把 64 位的数据的读写操作作为原子操作来对待,因此我们在编写代码的时候一般不把 long 和 double 变量专门声明位 volatile 多数情况下也是不会错的
Volatile 的实现原理
volatile 可见性实现
volatile 变量的内存可见性是基于内存屏障(Memory Barrier)实现的
- 内存屏障,又称内存栅栏,是一个 cpu 指令。
- 在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,jmm 为了保证在不同的编译器和 cpu 上有相同的结果,通过插入特定类型的内存屏障来禁止+特定类型的编译器重排序和指令重排序,插入一条内存指令屏障会告诉编译器和CPU:不管什么指令都不能和这条 Mermory Barrier 指令重排序。
写一段简单的java,声明一个 volatile 变量并赋值
public class Test {
private volatile int a;
public void update() {
a = 1;
}
public static void main(String[] args) {
Test test = new Test();
test.update();
}
}通过 hsdis 和 jjtwatch 工具可以得到编译后的汇编代码:
......
0x0000000002951563: and $0xffffffffffffff87,%rdi
0x0000000002951567: je 0x00000000029515f8
0x000000000295156d: test $0x7,%rdi
0x0000000002951574: jne 0x00000000029515bd
0x0000000002951576: test $0x300,%rdi
0x000000000295157d: jne 0x000000000295159c
0x000000000295157f: and $0x37f,%rax
0x0000000002951586: mov %rax,%rdi
0x0000000002951589: or %r15,%rdi
0x000000000295158c: lock cmpxchg %rdi,(%rdx) //在 volatile 修饰的共享变量进行写操作的时候会多出 lock 前缀的指令
0x0000000002951591: jne 0x0000000002951a15
0x0000000002951597: jmpq 0x00000000029515f8
0x000000000295159c: mov 0x8(%rdx),%edi
0x000000000295159f: shl $0x3,%rdi
0x00000000029515a3: mov 0xa8(%rdi),%rdi
0x00000000029515aa: or %r15,%rdi
......lock 前缀的指令会在多核处理器下会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存
- 写回内存的操作会让其他 cpu 里缓存了改内存地址的数据无效。
为了提高处理速度,处理器不和内存直接通信,而是先将系统内存的数据读到内存缓存(L1、L2 或其他)后在进行操作,但操作完不知道什么时候会刷新到内存。
如果声明了 volatile 的变量进行写操作,JVM 就会像处理器发送一条 lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存。
为了保证各个处理器的缓存是一致的,实现了缓存一致性协议(MESI),每个处理器通过嗅探在总线上传播的数据来检查缓存的值是不是过期了,当处理器发现自己缓存行的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
所有多核处理器下还会完成:当处理器发现本地缓存失效后,就会从内存中重新读取该变量获取最新的数据值
volatile 变量通过这样的机制就使得每个线程都能获得该变量的最新值。
缓存一致性
缓存是分段(line)的,一个段对应一快存储空间,称之为缓存行,它是 cpu 缓存中可分配的最小的存储单元,大小 32 个字节、64 字节、128 字节不等,这样 cpu 架构有关,通常来说是 64 字节,LOCK 指令因为锁总线效率太低,因此使用了多组缓存。为了使其行为看起来如同一组缓存那样,因为设计了 缓存一致性(MESI),缓存一致性有多种,但是日常处理的大多数计算机设备都属于“嗅探”协议。所有内存的传输都发生在一条共享的总线上,而所有的处理器都能看到这条总线。缓存本身是独立的,但是内存是共享资源,所有的内存访问都要经过仲裁(同一个指令周期中,只有一个 cpu 缓存可以读写内存)。cpu 缓存不仅仅在做内存传输的时候才与总线打交道,而是不同的在嗅探总线上发生的数据交换,跟踪其他缓存在做什么。当一个缓存代表它所属的处理器去读写内存时,其他处理器都会得到通知,它们以此来使自己的缓存保持同步,只有某个处理器写内存,其他处理器马上知道这块内存在他们的缓存段中已经失效。
volatile 有序性实现
volatile 的 happens-before 关系
happens-before 规则中有一条是 volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读(volatile写happens-before后续的volatile读)
//假设线程A执行writer方法,线程B执行reader方法
class VolatileExample {
int a = 0;
volatile boolean flag = false;
public void writer() {
a = 1; // 1 线程A修改共享变量
flag = true; // 2 线程A写volatile变量
}
public void reader() {
if (flag) { // 3 线程B读同一个volatile变量
int i = a; // 4 线程B读共享变量
……
}
}
}根据 happens-before规则,上面过程会建立 3 类 happens-before 关系

- 根据程序次序规则:1 happens-before 2 且 3 happens-before 4。
- 根据 volatile 规则:2 happens-before 3。
- 根据 happens-before 的传递性规则:1 happens-before 4。
因此以上规则,当线程 A 将 volatile 变量 flag 更改为true 后,线程 B 能够迅速感知。
volatile 禁止重排序
为了性能优化,JMM 在 不改变正确语义的前提下,会允许编译器和处理器对指令序列重排序。JMM 提供了内存屏障来阻止这种重排序。
java 编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。
JMM 会针对编译器制定 volatile 重排序规则表

"NO" 表示禁止重排序
为了实现 volatile 内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
| 内存屏障 | 说明 |
|---|---|
| StoreStore 屏障 | 禁止上面的普通写和下面的 volatile 写重排序。 |
| StoreLoad 屏障 | 防止上面的 volatile 写与下面可能有的 volatile 读/写重排序。 |
| LoadLoad 屏障 | 禁止下面所有的普通读操作和上面的 volatile 读重排序。 |
| LoadStore 屏障 | 禁止下面所有的普通写操作和上面的 volatile 读重排序。 |
对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM 采取了保守的策略。
- 在每个 volatile 写操作前面插入一个 StoreStore 屏障
- 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障
- 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障
- 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。


Volatile 的应用场景
使用 volatile 必须具备的条件
- 对变量的写操作不依赖于当前值
- 对变量没有包含在具有其他变量的不变式中
- 只有在状态真正独立于程序内其他内容时才能使用 volatile。
模式一:状态标志
也许实现 volatile 变量的规范使用仅仅是使用一个布尔状态标识,用于指示发生了一个重要的一次性事件,例如完成初始化或请求停机。
volatile boolean shutdownRequested;
......
public void shutdown(){shutdownRequested = true;}
public void doWork(){
while (!shutdownRequested) {
// do stufff
}
}模式二:一次性安全发布(one tim safe publication)
缺乏同步会导致无法实现可见性,这使得确认何时写入对象引用二部署原始值变得更加困难,在缺乏同步的情况下,可能会遇到某个对象引用的更新值(由另外一个程序写入)和该对象状态的旧值同时存在,(这就是造成著名的的双重检查锁问题的根源),其中对象引用在没有同步的情况下进行读操作,产生的问题是您可能会看到一个更新的引用,但是仍然会通过该引用看到不完全构造对象。
public class BackgroundFloobleLoader {
public volatile Flooble theFlooble;
public void initInBackground() {
// do lots of stuff
theFlooble = new Flooble(); // this is the only write to theFlooble
}
}
public class SomeOtherClass {
public void doWork() {
while (true) {
// do some stuff...
// use the Flooble, but only if it is ready
if (floobleLoader.theFlooble != null)
doSomething(floobleLoader.theFlooble);
}
}
}模式三:独立观察(independent observation)
安全使用 volatile 的另一种简单模式是定期发布观察结果供程序内部使用。例如,假设有一种环境传感器能够感觉环境温度,一个后台线程可能会每隔几秒读取一次该传感器,并更新包含当前文档的 volatile 变量。然后,其他线程可以读取这个变量,从而随时能够看到最新的温度值。
public class UserManager {
public volatile String lastUser;
public boolean authenticate(String user, String password) {
boolean valid = passwordIsValid(user, password);
if (valid) {
User u = new User();
activeUsers.add(u);
lastUser = user;
}
return valid;
}
}模式四:volatile bean 模式
在 volatile bean 模式中,JavaBean 的所有数据成员都是 volatile 类型的,并且 getter 和 setter 方法必须非常普通 —— 除了获取或设置相应的属性外,不能包含任何逻辑。此外,对于对象引用的数据成员,引用的对象必须是有效不可变的。(这将禁止具有数组值的属性,因为当数组引用被声明为 volatile 时,只有引用而不是数组本身具有 volatile 语义)。对于任何 volatile 变量,不变式或约束都不能包含 JavaBean 属性。
@ThreadSafe
public class Person {
private volatile String firstName;
private volatile String lastName;
private volatile int age;
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public int getAge() { return age; }
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public void setAge(int age) {
this.age = age;
}
}模式五:开销较低的读-写锁策略
volatile 的功能还不足以实现计数器。因为 ++x 实际上是三种操作(读、添加、存储)的简单组合,如果多个线程凑巧试图同时对 volatile 计数器执行增量操作,那么它的更新值有可能会丢失。 如果读操作远远超过写操作,可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。 安全的计数器使用 synchronized 确保增量操作是原子的,并使用 volatile 保证当前结果的可见性。如果更新不频繁的话,该方法可实现更好的性能,因为读路径的开销仅仅涉及 volatile 读操作,这通常要优于一个无竞争的锁获取的开销。
@ThreadSafe
public class CheesyCounter {
// Employs the cheap read-write lock trick
// All mutative operations MUST be done with the 'this' lock held
@GuardedBy("this") private volatile int value;
public int getValue() { return value; }
public synchronized int increment() {
return value++;
}
}模式六:双重检查(double-checked)
就是我们上文举的例子。
单例模式的一种实现方式,但很多人会忽略 volatile 关键字,因为没有该关键字,程序也可以很好的运行,只不过代码的稳定性总不是 100%,说不定在未来的某个时刻,隐藏的 bug 就出来了。
class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
syschronized(Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}