Java并发之volatile关键字
volatile关键字可以说是Java中的轻量级锁,实现Java的同步组件中有发挥了具体的作用。尽管volatile是非常的使用,然而它的原理总是让人困惑。换句话说,我们需要理解它的底层原理是不可或缺的。
01
volatile定义和原理
Java语言为了允许线程访问共享变量。一般来说,为了确保共享变量的一致性和实时更新,一个程序对于共享变量的操作必须对其他操作该变量的线程可见。换句话说,线程A对变量的操作结果,也可以被其他线程知道。
自Java5起,访问一个volatile变量将会产生内存屏障:这将有效的同步主内存中所有的变量的拷贝。如何理解这句话呢?
一个线程访问变量,首先从内存中拷贝一份变量的副本到自己工作内存中。例如在上图中,共有四个线程同时访问主内存中的变量,那么每一个线程都应该从主内存中拷贝一份自己要处理的变量。
那么volatile如何杜绝这一问题的产生呢?事实上,如果线程每一次对volatile变量修改,都会在总线上发送消息,表明这个值已经被一个线程修改了,然后将修改后的值写回到主内存;其他收到消息的线程将会认为自己工作内存中的副本无效,然后再回主内存中读取最新值;这样就避免了上述问题的发生。
02
volatile应用和危险性
在Java5后,volatile对于程序员产生了巨大的影响有两条:
1.对于volatile变量的get方法或者set方法就像加了锁一样。也就是单个的get或者set方法具有原子性。
2.提供了对volatile数组的第n个元素原子性的设置和获取。
也就是说,如果有这样的代码:
volatile int i;
...
public void set(int j){
int i=j;
}
public int get(){
return i;
}
与下面代码的等同:
public synchronized void set (int j) {
i = j;
}
public synchronized int get() {
return i;
}
就像每一个方法加了重量级锁一样。
尽管如此,这里也有很多对于volatile关键字不理解的程序员会认为下面的不具有原子性的代码具有原子性:
volatile int i;
...
i += 5;
虽然上面的看起来具有原子性,但是这和下面的代码相同:
// synchronized等同于锁
int i;
synchronized (temp) {
i = temp;
}
i += 5;
synchronized (temp) {
temp = i;
}
多个线程完全有机会出现更新丢失:
那么怎么样解决这个问题呢?Java中提供了原子类型,或者我们可以加锁;这些在以后的文章中会提到。
03
适用场景
前面两节中,已经分析了定义、原理以及错误案例。本节将会告诉你什么场景适合volatile变量。首先来看一下不需要volatile变量以及任何同步机制的场景:
1.不可变字段,即final修饰的常量
2.单线程访问的变量
· 3.volatile不适合复杂的操作,因为在操作期间需要阻止对变量的访问
有不适合的场景就有适合的场景,比较典型的场景有:
1.线程的标志,这个标志或许被另一个线程访问,比如优雅的停止线程
2.或者更新的值不依赖于旧值
在面试中我们进程被问及怎么样让两个线程如何交替的打印奇偶。下面我们给出一种简单的实现方式:
代码一:
public class MyThread {
private static volatile boolean flag=true;
private static volatile boolean isCanceled=false;
static class Odd extends Thread{
private int i=1;
public void run() {
while(!isCanceled)
{
while(!flag) {
System.out.println("the odd thread print:" + i);
i+=2;
flag = true;
}
}
}
public void cancel()
{
isCanceled=true;
}
}
static class Even extends Thread{
private int i=0;
public void run() {
while(!isCanceled)
{
while(flag) {
System.out.println("the event thread print:" + i);
i+=2;
flag = false;
}
}
}
public void cancel()
{
isCanceled=true;
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
Odd odd=new Odd();
Even even=new Even();
even.start();
odd.start();
Thread.sleep(10);
even.cancel();
odd.cancel();
}
}
04
总结
volatile关键字只是保证了可见性,对于get和set单个的操作是具有原子性,但是对于其他复合操作,例如i++操作是不具有原子性的。对于可见性,Java内部并发包中应用十分广泛,理解并使用volatile关键字对程序员来说是一个挑战也是进入高阶程序员的必备要素。
点击上方蓝色字体,关注我们