Java volatile 关键字

学习笔记 《Effective Java 中文版 第2版》

volatile关键字的作用是告知 JIT 编译器不要对被标记的变量执行任何可能影响其访问顺序的优化。该关键字警告 JIT 编译器,该变量可能会被某个线程更改,所以任何对改变了的读写访问都需要忽略本地cache并直接对内存进行操作。但由于每次变量的访问都要跨越内存栅栏,因此会导致程序性能下降 。此外,在多个字段被多个线程并发访问的场景下,由于针对每个volatile字段的访问都是各自独立处理的,并且也无法将这些访问同意协调成一次访问,所以volatile关键字无法保证整体操作的原子性。 —— 《Java 虚拟机并发编程》

下面看一个例子

public class StopThread {
    private static boolean stopRequested;

    public static void main(String[] args) {
        Thread backgroundThread = new Thread(new Runnable() {

            @Override
            public void run() {
                int i = 0;
                while(!stopRequested) {
                    i++;
                }
                System.out.println("Thread end, i=" + i);
            }
        });
        backgroundThread.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        stopRequested = true;
    }
}

你可能期待这个程序运行大约一秒左右,之后主线程将stopRequested设置为true,致使后台线程的循环终止。但是实际上这个程序永远不会终止,因为后台线程永远在循环。

问题在于,由于没有同步,就不能保证后台线程何时看到主线程对stopRequested变量的值所做的改变。没有同步,虚拟机会将这个代码:

while(!done) 
    i++;

转变成这样:

if(!done) 
    while(true)
        i++;

这是可以接受的。这种优化称作提升(hoisting),真是HopSpot Server VM的工作。结果是个活性失败(liveness failure):这个程序无法前进。修正这个问题的一种方式是同步访问stopRequested域。这个程序会如期般在大约一秒钟之内终止:

//Properly synchronized cooperative thread termination
public class StopThread {
    private static boolean stopRequested;
    private static synchronized void requestStop() {
        stopRequested = true;
    }
    private static synchronized boolean stopRequested() {
        return stopRequested;
    }

    public static void main(String[] args) {
        Thread backgroundThread = new Thread(new Runnable() {

            @Override
            public void run() {
                int i = 0;
                while(!stopRequested()) {
                    i++;
                }
                System.out.println("Thread end, i=" + i);
            }
        });
        backgroundThread.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        requestStop();
    }
}

stopThread中被同步方法的动作即使没有同步也是原子的。换句话说,这些方法的同步只是为了它的通信效果,而不是为了互斥访问。虽然循环的每个迭代中的同步开销很小,还是有其它更正确的替代方法,它更加简洁,性能也可能更好。如果stopRequested被声明为volatile,第二种版本的StopThread中的锁就可以省略。虽然volatile修饰符不执行互斥访问,但它可以保证任何一线程在读取该域的时候都看到最近刚刚被写入的值:

//Cooperative thread termination with a volatile field
public class StopThread {
    private static volatile boolean stopRequested;

    public static void main(String[] args) {
        Thread backgroundThread = new Thread(new Runnable() {

            @Override
            public void run() {
                int i = 0;
                while(!stopRequested) {
                    i++;
                }
                System.out.println("Thread end, i=" + i);
            }
        });
        backgroundThread.start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        stopRequested = true;
    }
}

在使用volatile的时候务必要小心。考虑下面的方法,假设它要产生序列号:

//Broken - requires synchronization
private static volatile int nextSerialNumber = 0;
public static int generateSerialNumber() {
    return nextSerialNumber++;
}

这个方法的目的是要确保每个调用都返回不同的值(只要不超过2^32个调用)。Java语言规范保证读或写一个基本变量是原子的,除非这个变量的烈性为long或者double。这个方法的状态只包含一个可原子访问的域:nextSerialNumerber,这个域的所有可能的值都是合法的。因此不需要任何同步来保护它的约束条件。然而,如果没有同步,这个方法仍然无法正常工作。

问题在于,增量操作符(++)不是原子的。它在nextSerialNumber域中执行两个操作:首先读取值,然后写回一个新值。如果第二个线程在第一个线程读取旧值和写回新值期间读取这个域,第二个线程就会与第一个线程一起看到同一个值,并返回相同的序列号。这就是安全性失败(safety failure):这个程序会计算出错误的结果。

修正的方法有两种,一种是使用synchronized修饰符,这个前面已经介绍过了;还有一种是使用AtomicLong类,它是java.util.concurrent.atomic的一部分,对于上面的这个例子,它的执行效率比synchronized更高。

private static final AtomicLong nextSerialNum = new AtomicLong();
public static long generateSerialNumber() {
    return nextSerialNum.getAndIncrement();
}

你可能感兴趣的:(Java)