volatile和synchronized到底啥区别?
1)volatile 与 synchronized 在处理哪些问题是相对等价的?
2)为什么说 volatile 是 synchronized 弱同步的方式?
3)volatile 除了可见性问题,还能解决什么问题?
4)二者我要如何选择使用?
都听过【天上一天,地下一年】,
假设 CPU 执行一条普通指令需要一天,那么 CPU 读写内存就得等待一年的时间。
受【木桶原理】的限制,在CPU眼里,程序的整体性能都被内存的办事效率拉低了,
为了解决这个短板,
硬件同学也使用了我们做软件常用的提速策略——使用缓存Cache
Java 内存模型(JMM)
CPU 增加了缓存均衡了与内存的速度差异,这一增加还是好几层。
此时内存的短板不再那么明显。但随之却带来很多问题
看上图,每个核都有自己的一级缓存(L1 Cache),
有的架构里面还有所有核共用的二级缓存(L2 Cache)。
使用缓存之后,当线程要访问共享变量时,如果 L1 中存在该共享变量,就不会再逐级访问直至主内存了。
所以,通过这种方式,就补上了访问内存慢的短板
具体来说,线程读/写共享变量的步骤是这样:
1.从主内存复制共享变量到自己的工作内存
2.在工作内存中对变量进行处理
3.处理完后,将变量值更新回主内存
假设现在主内存中有共享变量 X, 其初始值为 0
线程1先访问变量 X, 套用上面的步骤就是这样:
L1 和 L2 中都没有发现变量 X,直到在主内存中找到
拷贝变量 X 到 L1 和 L2 中
在 L1 中将 X 的值修改为1,并逐层写回到主内存中
此时,在线程 1 眼中,X 的值是这样的:
接下来,线程 2 同样按照上面的步骤访问变量 X
1.L1 中没有发现变量 X
2.L2 中发现了变量X
3.从L2中拷贝变量到L1中
4.在L1中将X 的值修改为2,并逐层写回到主内存中
此时,线程 2 眼中,X 的值是这样的:
结合刚刚的两次操作,当线程1再访问变量x,我们看看有什么问题
此刻,如果线程 1 再次将 x=1回写,就会覆盖线程2 x=2 的结果,
同样的共享变量,线程拿到的结果却不一样(线程1眼中x=1;线程2眼中x=2),
这就是共享变量内存不可见的问题
先来说说最熟悉的 synchronized 关键字
synchronized 关键字解决共享变量内存可见性问题
1)【进入】synchronized 块的内存语义是把在 synchronized 块内使用的变量从线程的工作内存中清除,
从主内存中读取
2)【退出】synchronized 块的内存语义事把在 synchronized 块内对共享变量的修改刷新到主内存中
volatile
当一个变量被声明为 volatile 时:
1)线程在【读取】共享变量时,会先清空本地内存变量值,再从主内存获取最新值
2)线程在【写入】共享变量时,不会把值缓存在寄存器或其他地方(就是刚刚说的所谓的「工作内存」),
而是会把值刷新回主内存
有种换汤不换药的感觉,你看的一点都没错
当使用 synchronized 或 volatile 后,多线程操作共享变量的步骤就变成了这样
简单点来说就是不再参考 L1 和 L2 中共享变量的值,而是直接访问主内存
public class ThreadSafeInteger {
// 共享变量 value
private int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
使用volatile改造:
public class ThreadSafeInteger {
// 共享变量 value
private volatile int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
使用synchronized改造:
public class ThreadSafeInteger {
// 共享变量 value
private int value;
public synchronized int getValue() {
return value;
}
public synchronized void setValue(int value) {
this.value = value;
}
}
这两个结果是完全相同,在解决【当前】共享变量数据可见性的问题上,二者算是等同的
@Slf4j
public class VisibilityIssue {
private static final int TOTAL = 10000;
// 即便像下面这样加了 volatile 关键字修饰不会解决问题,因为并没有解决原子性问题
private volatile int count;
public static void main(String[] args) {
VisibilityIssue visibilityIssue = new VisibilityIssue();
Thread thread1 = new Thread(() -> visibilityIssue.add10KCount());
Thread thread2 = new Thread(() -> visibilityIssue.add10KCount());
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
log.error(e.getMessage());
}
log.info("count 值为:{}", visibilityIssue.count);
}
private void add10KCount() {
int start = 0;
while (start++ < TOTAL) {
this.count++;
}
}
}
运行代码,你会发现,count的值始终是处于1w和2w之间的
在方法add10KCount上添加synchronized count 结果就是 2w
通过 volatile 和 synchronized 关键字以同样形式修饰,怎么有的可以带来相同结果,有的却不能呢?
二者的不同:
count++ 程序代码是一行,但是翻译成 CPU 指令确是三行
synchronized 是独占锁/排他锁(就是有你没我的意思),
同时只能有一个线程调用 add10KCount 方法,
其他调用线程会被阻塞。
所以三行 CPU 指令都是同一个线程执行完之后别的线程才能继续执行,
这就是通常说说的 原子性 (线程执行多条指令不被中断)
volatile 是非阻塞算法(也就是不排他),
当遇到三行 CPU 指令自然就不能保证别的线程不插足了,
这就是通常所说的,volatile 能保证内存可见性,但是不能保证原子性
什么时候才能用volatile关键字呢?
如果写入变量值不依赖变量当前值,那么就可以用 volatile
如果写入变量值不依赖变量当前值,那么就可以用 volatile
如果写入变量值不依赖变量当前值,那么就可以用 volatile
上面 count++ ,是获取-计算-写入三步操作,也就是依赖当前值的,所以不能靠volatile 解决问题
synchronized 是排他的,线程排队就要有切换,这个切换就好比上面的例子,要完成切换,
还得记准线程上一次的操作,很累CPU大脑,这就是通常说的上下文切换会带来很大开销
volatile 就不一样了,它是非阻塞的方式,所以在解决共享变量可见性问题的时候,
volatile 就是 synchronized 的弱同步体现了