写一个简单代码:
public class Main {
private static boolean RUN = true;
private static long n = 0L;
public static void main(String[] args) {
Main m = new Main();
m.test();
System.out.println("开始赋值");
n = 6666;
RUN = false;
}
private void test() {
new Thread(new Runnable() {
@Override
public void run() {
while (RUN) {
System.out.print((n++) + ",");
}
}
}).start();
}
}
这段程序运行后会输出什么结果???实际运行后输出结果不可预料!有时候会输出:
开始赋值
而有时候则可能输出:
开始赋值
0,
输出的结果完全不可控、不确定。
又如下面代码片段:
int n = 0;
n = 1;
n = 2;
n = 3;
n = 4;
在程序模块中,这5行代码也许代表五个操作步骤,但是在现代Java编译器中,编译器将优化,对CPU、寄存器中的操作指令进行优化,将上述五个操作指令步骤的执行,合并成一条机器执行代码(最后一条,n = 4)!
造成以上现象的关键原因是,若在没有同步的情况下,Java的编译器以及运行时CPU将对代码最终执行的机器码进行令人意想不到的“优化”,即重排序。现代Java编译器和现代多核CPU架构均在运行时进行这样的重排序,之所以这么做的原因是为了充分发挥现代多核CPU的计算能力,但随之而来的问题就是上述现象的发生,在多线程环境中,某些未经完善考虑的代码,会引发严重的多线程安全问题。
至此要引出一个Java多线程中的重要概念:原子操作。在Java中,如 int n = 1; 这样的操作是原子操作,所谓原子操作,简单的说,编译器和CPU认为这些代码所表示的操作是不可分割的。原子是物质不可切分的单位,原子操作取不可切分的寓意。但是像Java代码中: n++,是非原子操作,因为 n++ 相当于 n = n + 1, n = n + 1是可以切分的、非原子操作。
JVM 1.2之前,Java内存模型总是从主存读取变量。随着JVM后续版本优化,基于现在1.2版本之后的JVM多线程环境下volatile关键字使用变得非常重要。
当前Java内存模型,线程可以把变量保存在本地内存(如机器的寄存器),而不是直接到主存中进行读写。这就很可能造成一个线程在主存中修改了一个变量的值,而另外一个线程还继续使用它在本地寄存器中该变量值的拷贝,造成数据不一致。
要解决这个问题,需要在程序中把该变量声明为volatile(不稳定的)即可,指示JVM,这个变量不稳定,每次使用它都得到主存进行读取。通常多线程环境下各任务间共享的数据原子上都应该加volatile关键字修饰。
volatile修饰的变量在每次被线程访问时,都强迫从主存中重读该成员变量的最新值。而且当变量发生变化时,强迫线程将变化值回写到主存。这样在任何时刻,多个线程总是看到某个成员变量的同一个值。
Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。
这样当多个线程同时存取共享成员变量值时候,就必须要注意让多个线程及时感知共享成员变量的变化。
volatile关键字就是提示JVM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
在两个以上线程访问的成员变量使用volatile。当要访问的变量已在synchronized代码块中,或者为常量时,不必使用。
由于使用volatile屏蔽掉了JVM必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。
Java 语言包含两种同步机制:synchronized同步块(或方法)和 volatile 变量。这两种机制的都是为了实现代码线程的安全性。其中volatile变量的同步性较差(但有时它更简单并且开销更低),而且在使用时候也更容易出错。
Java 语言中的 volatile变量可以被视作是一种 “轻量级的synchronized”,与synchronized 代码块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能仅是 synchronized 的一部分。
锁提供了两种主要特性:互斥(mutual exclusion)和可见性(visibility)。“互斥”即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问,一次就只有一个线程能够使用该共享数据。“可见性”要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的,如果没有同步机制提供可见性保证,线程看到的共享变量可能是修改前的值或不一致的值,这将引发多线程环境下数据一致性问题。
volatile变量特性:volatile不保证原子操作
volatile变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现volatile变量的最新值。volatile变量可用于提供线程安全,但是只能应用于非常有限的一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。出于简易性的考虑,开发者可能倾向于使用volatile变量而不是锁。当使用volatile变量而非锁时,某些习惯用法更加易于编码和阅读。此外,volatile变量不会像锁那样造成线程阻塞,因此也很少造成可伸缩性问题。在某些情况下,如果读操作远远大于写操作,volatile变量还可以提供优于锁的性能优势。