前言
相信很多人对于volatile关键字既熟悉又陌生,熟悉是对这个名字很熟悉,陌生是对他的原理和用法很陌生,最近几天通过查阅大量资料和书,终于对volatile有了一定的理解,写此文一来做了记录,二来使大家减少学习成本,有问题和心得欢迎在评论区一起互相交流探讨。
volatile为什么不能保证原子性?
现在我们的手机都是多核的,也就是说同时有好几颗CPU在工作,每颗CPU都有自己的Cache高速缓存,因为CPU的速度特别快,而内存的读取操作相对于CPU的运算速度来说很慢,所以就会拖累CPU的效率,引入Cache就是为了解决这个问题的,CPU先把需要的数据从内存中读到Cache中,然后直接和Cache来打交道,Cache的速度很快,因此可以保证CPU的工作效率,当Cache中的数据改变后,再将被改变的数据写回内存中。
首先我们分析一下多线程在访问一个普通的(没有加volatile修饰符)的变量的过程;
1.CPU1和CPU2先将count变量读到Cache中,比如内存中count=1,那么现在两个CPU中Cache中的count都等于1 。
2.CPU1和CPU2分别开启一个线程来对count进行自增操作,每个线程都有一块自己的独立内存空间来存放count的副本。
3.线程1对副本count进行自增操作后,count=2 ; 线程2对副本count进行自增操作后,count=2
4.线程1和线程2将操作后的count写回到Cache缓存的count中 。
5.CPU1和CPU2将Cache中的count写回内存中的count。
那么问题就来了,线程1和线程2操作的count都是一个count副本,这两个副本的值是一样的,所以进行了两次自增后,写回内存的count=2。而正确的结果应该为count=3。这就是多线程并发所带来的问题
如果变量count用volatile修饰了可以解决这个问题吗?
如果一个变量加了volatile关键字,就会告诉编译器和JVM的内存模型:这个变量是对所有线程共享的、可见的,每次JVM都会读取最新写入的值并使其最新值在所有CPU可见。我们再来看一下线程在访问一个加了volatile修饰符的变量的过程;
当count用volatile关键字修饰后,CPU1对count的值更新后,在写回内存的同时会通知CPU2 count值已经更新了,你需要从内存中获取count最新的值!
注意:这里说CPU1通知CPU2其实是不严谨的,其实这是缓存一致性机制在其作用,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行数据时,会使缓存行无效,当CPU1将新数据写回内存后,会修改该数据在内存中的内存地址,CPU2通过嗅探在总线上传播的数据来检查自己的缓存行对应的内存地址是否被修改,如果被修改则将CPU2的该数据缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到CPU2的缓存行里。其实并不是CPU1通知CPU2,而是CPU2自己去嗅探。
其实大家只要明白了原理,怎么说也无所谓,就像好多地方都说volatile修饰的变量,线程直接和内存交互,不会保存副本。而实际上线程还是会保存副本,只不过CPU每次都会从内存中拿到最新的值,并且改变数据之后立马写回内存,看上去就像线程直接和内存交互一样。
然后CPU2中的线程如果需要使用到count的时候,就会再从内存中读取count的值来更新自己的Cache。这看上去似乎解决了我们的问题,其实问题依然存在,我们来分析一下:
比如当前count=1,CPU1和CPU2的Cache中的count都等于1,CPU1中的线程1对count进行了自增操作,然后CPU1更新了内存中count的值,并且通知CPU2 count的值已经改变,然后CPU2从内存中将count=2读到了Cache中,并且线程2开始执行count的自增操作,而就在CPU2刚刚将count的值读回Cache的时候,CPU1又更新了count的值,此时count=3,并且通知CPU2,但是此时线程2已经开始执行了,线程2已经将count=2拷贝到自己的内存空间中了,所以即使CPU2再次更新自己Cache中的count=3,也来不及了,线程2操作的是他自己内存空间中的count副本,所以线程2给count做完自增操作后,将count=3并且写回Cache,CPU2更新内存中的count。此时count的值应该是4,然而CPU2更新完count的值后仍然等于3,这样就出现了错误。我们考虑的是只有两颗CPU的情况,但是现在市面上已经有8核手机了!如果8颗CPU同时工作的话,错误会更严重!
Atomic如何保证原子性?
要知道Atomic是如何保证原子性的,我们还得先来了解一下什么是CAS。CAS是一种有名的无锁算法(Compare and Swap)。他的核心思想是:当多个线程尝试使用CAS同时更新同一个变量时,只有一个线程能更新变量的值,而其他线程都失败,失败的线程并不会挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
还是我们上面的例子:
CPU2将count=3往内存写时,CAS中的3个操作数:V=3,A=2,B=3。当前内存中的count=3,CPU2的预期值应该是count=2,然而3不等于2,所以CPU2在这次竞争中失败。不再更新内存值。当线程2再次执行时,CPU2会从内存中获取最新的count值。这样就保证了线程安全。
Atomic正是引入了CAS无锁算法,我们来看一下AtomicInteger这个类:
private volatile int value;
AtomicInteger类内部使用一个被volatile修饰的int类型变量value来记录我们的值,为什么要使用volatile呢?
/**
* Gets the current value.
*
* @return the current value
*/
public final int get() {
return value;
}
AtomicInteger中有个get()方法,前面我们分析了,使用volatile关键字修饰的变量,在每次获取他的值的时候,我们都可以获取到他在内存中最新的值,既然有get()方法,那么肯定得有set()方法吧
/**
* Sets to the given value.
*
* @param newValue the new value
*/
public final void set(int newValue) {
value = newValue;
}
set()方法也很简单,只是把一个新的值赋给了value,可是刚才我们分析了那么半天,他并没有保证原子操作啊!别着急,关键就在这里:
/**
* Atomically sets to the given value and returns the old value.
*
* @param newValue the new value
* @return the previous value
*/
public final int getAndSet(int newValue) {
for (;;) {
int current = get();
if (compareAndSet(current, newValue))
return current;
}
}
是不是很熟悉!像不像刚才我们说的CAS算法,先是调用get()方法获取内存中最新的值,然后再调用compareAndSet方法来更新值,我们来看看compareAndSet方法;
/**
* Atomically sets the value to the given updated value
* if the current value {@code ==} the expected value.
*
* @param expect the expected value
* @param update the new value
* @return true if successful. False return indicates that
* the actual value was not equal to the expected value.
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
看见没!compareAndSwap不就是刚才我们讲的CAS算法吗!这样就能保证原子操作了!AtomicInteger还有很多方法,最终都是调用了compareAndSwap方法!
比如原子操作的++value;
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return next;
}
}
原子操作版的value++;
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
如何正确的使用volatile关键字?
我们来看一个例子:
public class VolatileDemo {
int x = 0 ;
//注意:这里的b没有被volatile修饰
boolean b = false;
/**
* 写操作
*/
private void write(){
x = 5;
b = true;
}
/**
* 读操作
*/
private void read(){
//如果b=false的话,就会无限循环,直到b=true才会执行结束,会打印出x的值
while(!b){
}
System.out.println("x="+x);
}
public static void main(String[] args) throws Exception {
final VolatileDemo volatileDemo = new VolatileDemo();
//线程1执行写操作
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
volatileDemo.write();
}
});
//线程2执行读操作
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
volatileDemo.read();
}
});
//我们让线程2的读操作先执行
thread2.start();
//睡1毫秒,为了保证线程2比线程1先执行
TimeUnit.MILLISECONDS.sleep(1);
//再让线程1的写操作执行
thread1.start();
thread1.join();
thread2.join();
//等待线程1和线程2全部结束后,打印执行结束
System.out.println("执行结束");
}
}
注意我们的b没有用volatile修饰,我们先启动了线程2的读操作,后启动了线程1的写操作,由于线程1和线程2会保存x和b的副本到自己的工作内存中,线程2执行后,由于他副本b=false,所以会进入到无限循环中,线程1执行后修改的也是自己副本中的b=true,然而线程2无法立即察觉到,所以执行上面代码后,不会打印“执行结束”,因为线程2一直在执行!
运行之后会一直出于运行状态,并且没有打印“执行结束”
如果我们将b用volatile关键字修饰后,奇迹就出现了
//注意:这里的b被volatile修饰
volatile boolean b = false;
此时的流程会是这样子
给b加了volatile关键字修饰后,线程1对b做了修改,然后会立即更新内存中的值,线程2通过嗅探发现自己的副本已经过期了,然后重新从内存中拿到b=true的值,然后跳出while循环,执行结束!
参考资料;
《Java并发编程艺术》
为什么volatile不能保证原子性而Atomic可以?
非阻塞同步算法与CAS(Compare and Swap)无锁算法
推荐阅读;
Android行业薪资现状,月薪2万属于低收入?
上海大厂Android面试经历;华为+小米+映客+抖音
别人花了几万元学的Android高级技术,我帮你们免费弄来了!
网友评论