为什么会有volatile 与 synchronized这两个关键字
cpu执行一条指令的时间远远快于cpu读写内存的时间,计算机届解决问题永恒的方案常常是加中间件,对于解决cpu与内存之间效率问题也是同理,在内存和cpu之间增加缓存,来解决两者之间效率的差异,避免内存低效率进而形成木桶效应。
于是这就带来了一个隐患,如下图所示:
这时候下面的cpu也要操作这个变量x,结果在二级缓存中找到,加载回自己的一级缓存,也开始了自己的操作。
因为下面的cpu加载到的值是二级缓存的,并不上面的cpu操作后的结果。这就导致两个cpu最终无论谁进行写操作都会覆盖对方的结果。原因也很简单,加了缓存出现了可见性问题,双方都不知道彼此之间的变化。
所以,这时候我们就得保证可见性,如果加volatile,当线程需要读取共享变量时,就会清空本地缓存,从主存中获取共享变量值。同样的,当线程一顿操作后也会直接将变量写回主存中并不会经过寄存器或者其他。
synchronized工作原理也差不多,进入synchronized语句块要操作共享变量时,也会将该变量从线程的工作内存中清除,去主存中读取。退出synchronized语句块时,也直接将结果写回主存中。
volatile 与 synchronized 在处理哪些问题是相对等价的
有了上面的描述,我想这张图应该就很好的解释了这道题了。只要用了这两个关键字,操作共享变量时我们完完全全可以看作绕过了缓存层。
为什么说 volatile 是 synchronized 弱同步的⽅式?
回答这个问题前,我们先来聊聊:既然两个关键字在上面的描述来看效果一样的,为什么java还要设计两个关键字来解决这个问题呢?很明显,这两个关键字是有区别的,我们不妨看看这样两段代码
public class VisiblityIssue {
//由于现代cpu性能的原因,测试的时候这个数字尽量大一些 才能看到效果
private static final int TOTAL = 10000;
private volatile int count;
public static void main(String[] args) {
VisiblityIssue v = new VisiblityIssue();
Thread t1 = new Thread(v::addCount);
Thread t2 = new Thread(v::addCount);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(v.count);//14003
}
private void addCount() {
int start = 0;
while (start++ < TOTAL) {
this.count++;
}
}
}
复制代码
可以看到使用volatile操作共享变量进行自增操作时,结果总是会小于最终结果。而synchronized却没有这个问题,如下代码所示:
public class VisiblityIssue {
//由于现代cpu性能的原因,测试的时候这个数字尽量大一些 才能看到效果
private static final int TOTAL = 10000;
private /*volatile*/ int count;
public static void main(String[] args) {
VisiblityIssue v = new VisiblityIssue();
Thread t1 = new Thread(v::addCount);
Thread t2 = new Thread(v::addCount);
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(v.count);//14003
}
private synchronized void addCount() {
int start = 0;
while (start++ < TOTAL) {
this.count++;
}
}
}
复制代码
原因也很简单,我们不妨写一下这样一段代码使用javap命令查看自增操作cpu指令
javac VisiblityIssue.java
javap -c VisiblityIssue.class
复制代码
public class VisiblityIssue {
private static final int TOTAL = 10000;
private /*volatile*/ int count;
public static void main(String[] args) {
VisiblityIssue v = new VisiblityIssue();
v.count++;
}
}
复制代码
键入命令后可以看到这样的结果,不难看出自增操作在底层需要三条jvm指令,volatile只能保证可见性不能保证原子性
Compiled from "VisiblityIssue.java"
public class com.example.volatileAndSyn.visible.VisiblityIssue {
public com.example.volatileAndSyn.visible.VisiblityIssue();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/example/volatileAndSyn/visible/VisiblityIssue
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: dup
10: getfield #4 // Field count:I 从对象中获取字段count
13: iconst_1
14: iadd //count+1
15: putfield #4 // Field count:I 为指定类的实例变量赋值
18: return
}
复制代码
这时候我们就可以引出这样一个结论:synchronized是独占锁/排他锁,当在synchronized块执行自增操作时,其他线程都必须等到当前线程执行完才能执行,这就是原子性。而volatile是非阻塞算法,遇到三行指令时不能保证别的线程不插足,这就会导致上述代码的问题。所以要使用volatile时,你得保证写入变量不依赖当前变量,且效率会比synchronized高。
可能听到这里你还是不明白,什么叫非阻塞算法,什么又叫阻塞算法。举个栗子,当你写着代码途中要去办事,你就必须记录当前代码写到哪以便一会接着思路写。而去办事的过程中你又接到一个电话,你又得记录当前办事进度去接电话。就是这样事情穿插的进行,你的大脑必须在事情切换的时候记录进度。
synchronized也是如此,因为它是排他的,导致线程切换时,就需要记录当前线程执行进度。再进行上下文切换,而上下文切换开销是很大的(好比脑子记录当前工作进度进而做其他的事情)。
而volatile是非阻塞的,他的可见性并不是通过锁来保证的,所以我们说volatile是synchronized的弱同步方式。
volatile 除了可⻅性问题,还能解决什么问题?
保证编译优化时出现的指令重排序,即保证有序性,具体可以参考笔者这篇文章 # 大哥,原来这个就是volatile啊