2. 可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。 比如:用 volatile
修饰的变量,就会具有可见性。volatile
修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile
只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0
;之后有一个操作 a++
;这个变量 a
具有可见性,但是 a++
依然是一个非原子操作,也就是这个操作同样存在线程安全问题。
在 Java
中 volatile
、synchronized
和 final
实现可见性。
3. 有序性
即程序执行的顺序按照代码的先后顺序执行。
举个例子:
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
上面代码定义了一个 int
型变量,定义了一个 boolean
类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1
是在 语句2
前面的,那么 JVM
在真正执行这段代码的时候会保证 语句1
一定会在 语句2
前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder
)。
指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
Java
语言提供了 volatile
和 synchronized
两个关键字来保证线程之间操作的有序性,volatile
是因为其本身包含“禁止指令重排序”的语义,synchronized
是由“一个变量在同一个时刻只允许一条线程对其进行 lock
操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。
2. volatile 关键字解析
volatile
可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM
底层volatile
是采用“内存屏障”来实现的。
一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile
修饰之后,那么就具备了两层语义:
- 保证不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这个新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
Java
语言提供了一种稍弱的同步机制,即 volatile
变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为 volatile
类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile
变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取 volatile
类型的变量时总会返回最新写入的值。
在访问 volatile
变量时不会执行加锁操作,所以不会造成线程阻塞,所以 volatile
变量是一种比 synchronized
关键字更轻量级的同步机制。
当对非 volatile
变量进行读写的时候,每个线程先从内存拷贝变量到 CPU
缓存中。如果计算机有多个 CPU
,每个线程可能在不同的 CPU
上被处理,这意味着每个线程可以拷贝到不同的 CPU cache
中。
而声明变量是 volatile
的,JVM
保证了每次读变量都从内存中读,跳过 CPU cache
这一步。
2.1 volatile 实现机制
“观察加入 volatile
关键字和没有加入 volatile
关键字时所生成的汇编代码发现,加入volatile
关键字时,会多出一个 lock
前缀指令”,lock
前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供 3
个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他
CPU
中对应的缓存行无效。
3.2 volatile 性能
volatile
的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
3. 使用 volatile 关键字的场景
synchronized
关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而 volatile
关键字在某些情况下性能要优于 synchronized
,但是要注意 volatile
关键字是无法替代 synchronized
关键字的,因为 volatile
关键字无法保证操作的原子性。通常来说,使用 volatile
必须具备以下 2
个条件:
- 对变量的写操作不依赖于当前值
- 该变量没有包含在具有其他变量的不变式中
实际上,这些条件表明,可以被写入 volatile
变量的这些有效值独立于任何程序的状态,包括变量的当前状态。事实上,我的理解就是上面的 2
个条件需要保证操作是原子性操作,才能保证使用volatile
关键字的程序在并发时能够正确执行。
3.1 场景一:状态标记量
volatile boolean flag = false;
//线程1
while(!flag){
doSomething();
}
//线程2
public void setFlag() {
flag = true;
}
根据状态标记,终止线程。
3.2 场景二:单例模式中的double check
class Singleton{
private volatile static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if(instancenull) {
synchronized (Singleton.class) {
if(instancenull)
instance = new Singleton();
}
}
return instance;
}
}
4. 总结
本文只是对 volatile
的简单使用进行一个简单的总结,更多的学习可以参照文章:
最后
本人也收藏了一份Java面试核心知识点来应付面试,借着这次机会可以送给我的读者朋友们:
目录:
Java面试核心知识点
一共有30个专题,足够读者朋友们应付面试啦,也节省朋友们去到处搜刮资料自己整理的时间!
Java面试核心知识点
Java面试核心知识点
一共有30个专题,足够读者朋友们应付面试啦,也节省朋友们去到处搜刮资料自己整理的时间!
[外链图片转存中…(img-17iUckTc-1723295467596)]
Java面试核心知识点