前言
这篇记录笔者在学习Java并发编程路上遇到的synchronized和volatile关键字
一、为什么要使用synchronized和volatile关键字
我们都知道多线程环境下存在线程安全问题,即多个线程对同一个变量进行读写操作时,可能并不会按照我们预期的访问执行顺序对变量进行操作,从而造成错误;而我们对变量进行上锁也可能无法保证上锁后的结果就一定是线程安全,至于为什么,大家可以看一看这篇博客
上了锁就一定安全吗
二、相同点
synchronized
在上文提及的博客中,笔者提及了各自线程在读取共享内存中的变量,可能为了提高访问速度,将变量保存到自己的线程内存中而不及时进行写回操作
而synchronized关键字就是为了避免这样情况的发生
synchronized关键字既能够保证对变量修改的原子性,也同时能够保证对变量修改的可见性
原子性:
- 一个操作一旦开始,就不会被其它线程干扰。简单粗暴点理解,要么不执行,要么执行到底。
可见性
-一个线程对一个共享变量的修改,能够及时被其他变量所看见
那么,我们来看看sychronized关键字如何解决共享变量内存可见性的问题
- 【进入】synchronized 块的内存语义是把在 synchronized 块内使用的变量从线程的工作内存中清除,从主内存中读取
- 【退出】synchronized 块的内存语义事把在 synchronized 块内对共享变量的修改刷新到主内存中
volatile
当一个变量被声明为 volatile 时:
- 线程在【读取】共享变量时,会先清空本地内存变量值,再从主内存获取最新值
- 线程在【写入】共享变量时,不会把值缓存在寄存器或其他地方(就是刚刚说的所谓的「工作内存」),而是会把值刷新回主内存
似乎看到这一步,synchronized和volatile关键字过程都是这样
简单来说,就是保证各个线程对共享变量进行写回操作,确保其直接访问主内存
三、不同点
具体的不同我们通过例子来进行比较
对一个value值进行设置
public class ThreadNotSafeInteger {
/**
* 共享变量 value
*/
private int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = 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;
}
}
在此问题上,二者导向的结果相同,能够保证共享变量数据的线程安全
那么我们来看什么情况下,synchronized和volatile关键字导向的结果会不同
来看下面的情况
@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 ++;
}
}
}
在这种情况下,使用volatile关键字并不会达到我们预期,在该情况下,两个线程通过count++得到的结果一直会在1w~2w之间波动
我们再以synchronized对其进行修改
@Slf4j
public class VisibilityIssue {
private static final int TOTAL = 10000;
private int count;
//... 同上
private synchronized void add10KCount(){
int start = 0;
while (start ++ < TOTAL){
this.count ++;
}
}
}
我们发现,synchronized关键字能够完成我们对其要求的任务,即保证变量值得到正确修改,这是为什么呢(count++最终为2w)
要解决这个问题,我们需要知道,count++这一行代码,实际上包含了三个原子性CPU操作指令:
- 取出值
- 修改值
- 写回值
而这三步,在多线程环境下,线程对CPU进行抢占时,可能会发生线程A刚刚取出值和修改值,线程B又去取出值进行一番操作,最终造成了count值在1w~2w之间波动的结果
而synchronized能够完成该任务是因为,synchronized是排他锁,即在synchronized内部的代码运行期间,其他线程是不能抢占式地运行synchronized中的代码的
但是synchronized排他性,线程排队就要进行切换,切换就会有开销,从而带来大量的因为上下文切换导致的开销
volatile则是以非阻塞的方式,进行解决共享变量可见性的问题,也就是说volatile是synchronized的弱同步方式的体现;
总结
synchronized可以保证原子性和排他性,但是切换开销较大,对于性能的消耗较多;
volatile以非阻塞的方式完成共享变量可见性的问题,负荷较小,适合原子性的操作,或者说适合一些对值进行修改不依赖其本身值的操作