为什么要额外写一篇文章来研究volatile
呢?是因为这可能是并发中最令人困惑以及最被误解的结构。我看过不少解释volatile
的博客,但是大多数要么不完整,要么难以理解。我会从并发中最重要的一些因素开始说起:
原子性
原子性是不可分割的操作。它们要么全部实现,要么全部不实现。Java中原子操作的最佳例子是将一个值赋给变量。
可见性
可见性是指:无论是哪个线程对一个共享的变量作出的修改或是带来的影响,读其他的线程都是可见的。
有序性
有序性是指源码中指令是否会被编译器出于优化而改变执行顺序。有可能一个线程中的动作相对于另一个线程出现乱序。
现在举一个例子来理解这些因素:
public class MyApp
{
private int count = 0;
public void upateVisitors()
{
++count; //increment the visitors count
}
}
Hint: read-modify-write
这一段代码中有一个试图更新应用(网页)的访客数量的方法。这段代码的问题在于++count
指令不是原子性的,它包含三条独立的指令:
temp = count; (read)
temp = temp + 1; (modify)
count = temp; (write)
因此,当一个线程正在执行此操作时,此指令可以被另一个线程预占。从而不是原子性操作。假设count的值为10,并且有如下的执行顺序:
我们会发现:在某个很不巧合的时刻,两个线程同时读取到了值(10),然后彼此将其值加一。所以在这个过程有一个递增的操作丢失了。当实际输出取决于线程交错的结果时,这种情况被称为竞争条件(race condition)。这里丢失了一次递增。那么并发的哪些方面在这里缺失了?原子性。再考虑一个创建单例的例子(当然也是不好的例子):
public Singleton getInstance()
{
if(_instance == null)
{
_instance = new Singleton();
}
}
Hint: check-then-act
再一次的,可能有两个线程都判断这实例为null,并且都进入了if代码块。这会导致两个实例的创建。这里的问题在于代码块不是原子性的,而且实例的变化对别的线程不可见。这种不能同时在多个线程上执行的部分被称为关键部分(critical section)。对于关键部分,我们需要使用synchronized块和synchronized方法。
还是原子性
为了确保原子性,我们通常使用锁来确保互斥。参考下面的例子,一个银行账户使用synchronized
方法上锁。
class BankAccount {
private int accountBalance;
synchronized int getAccountBalance() {
return accountBalance;
}
synchronized void setAccountBalance(int b) throws IllegalStateException {
accountBalance = b;
if (accountBalance < 0) {
throw new IllegalStateException("Sorry but account has negative Balance");
}
}
void depositMoney(int amount) {
int balance = getAccountBalance();
setAccountBalance(balance + amount);
}
void withdrawMoney(int amount) {
int balance = getAccountBalance();
setAccountBalance(balance - amount);
}
}
对共享变量balance
的访问通过锁来保护,从而数据竞争不会有问题。这个类有问题吗?是有的。假设一个线程调用depositMoney(50)
而另一个线程调用withdrawMoney(50)
,并且balance
的初始值为100。理想情况下操作完成后balance
应该为0。但是我们无法保证得到这个结果:
-
depositMoney
操作读取的balance
值为100 -
withdrawMoney
操作读取的balance
值也是100,它在此基础上减去50元并将其设为50元。 - 最终
depositMoney
在之前看到的balance
值的基础上加上50,并将其设为150。
再次因为没有保证原子性而丢失了一个更新。如果两种方法都被声明为同步,则将在整个方法期间确保锁定,并且改变将以原子方式进行。
再谈可见性
如果一个线程的操作对另一个线程可见,那么其他线程也会观察到它的所有操作的结果。考虑下面的例子:
public class LooperThread extends Thread
{
private boolean isDone = false;
public void run()
{
while( !isDone ) {
doSomeWork();
}
}
public void stopWork() {
isDone = true;
}
}
这里缺失了什么?假设LooperThread
的一个实例正在运行,主线程调用了stopWord
来中止它。这两个线程之间没有实现同步。编译器会以为在第一个线程中没有对isDone
执行写入操作,并且决定只读入isDone
一次。于是,线程炸了!部分JVM可能会这样做,从而使其变成无限循环。因此答案显然是缺乏可见性。
再谈有序性
有序性是关于事情发生的顺序。考虑下面的例子:
在上述情况下,线程2能打印出value = 0
吗?其实是有可能的。在编译器重新排序中result=true
可能会在value=1
之前出现。value = 1
也可能不对线程2可见,然后线程2将加载value = 0
。我们可以使用volatile
解决这个问题吗?
CPU架构(多层RAMs)
CPU现在通常多核,并且线程将在不同核心上运行。另外还有不同级别的高速缓存,如下图所示:
当一个volatile变量被任何线程写入一个特定的核心,所有其他核心的值都需要更新,因为每个核心都有其自己的缓存,该缓存内有变量的旧值。消息传递给所有内核以更新值。
volatile
根据Java文档,如果一个变量被声明为volatile,那么Java内存模型(在JDK 5之后)确保所有线程都看到变量的一致值。volatile就像是synchronized的一个亲戚,读取volatile数据就像是进入一个synchronized块,而写入volatile数据就像是从synchronized块中离开。当写入一个volatile值时,这个值直接写入主存而不是本地处理器的缓存,并且通过发送消息提醒其它内核的缓存该值的更新。Volatile不是原子性操作
volatile保证顺序性和可见性但是不保证互斥或是原子性。锁能保证原子性,可视性和顺序性。所以volatile不能代替synchronized。
volatile读与写volatile
提供了顺序性保障,这意味着编译器生成的指令不能以实际源代码指令定义的顺序以外的其他顺序执行操作结果。尽管生成的指令的顺序可能与源代码的原始顺序不同,但所产生的效果必须相同。我们还需要从Java Doc中观察以下关于读写的内容:
当一个线程读取一个volatile变量时,它不仅会看到volatile的最新变化,还会看到导致变化的代码的副作用。
我们需要了解以下有关读写volatile的内容:
- 当一个线程写入一个volatile变量,另一个线程看到写入,第一个线程会告诉第二个线程关于内存变化的内容,直到它执行写入该volatile变量。
- 在这里,线程2看到了线程1的内容。
我们可以声明 final 类型的volatile变量吗?
如果一个变量是final的,我们不能改变它的值,volatile就是确保对其他线程可见的共享变量的更改。所以这是不允许的,并会导致编译错误。
为什么我们在并发编程中声明long / double为volatile?
默认情况下long/double的读写不是原子性的。非原子性的double/long写操作会被当做两个写入操作:分别写入前32位和后32位。它可能会导致一个线程看到另一个线程写入的64位值的前32位,而第二个线程看到来自另一个线程写入的后32位。读写volatile的long/double类型变量总是原子性的。
Volatile vs Atomic类
public class MyApp
{
private volatile int count = 0;
public void upateVisitors()
{
++count; //increment the visitors count
}
}
如果我们将count声明为atomic
,这段代码可以正常运行吗?可以的,而且当对变量进行增加或减少操作时,最好使用atomic
类。AtomicInteger
通常使用volatile
或是CAS来实现线程安全。
想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注我的微信公众号!将会不定期的发放福利哦~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。