原子性
对基本数据类型的变量的读取和赋值时原子性操作,即这些操作是不可以被中断的,要么执行完毕,要么就不执行。看一下下面的代码,如下:
x=3; //语句1 y=x; //语句2 x++; //语句3
在上面3个语句中,只有语句1是原子性操作,其他两个语句都不是原子性操作。
语句2虽说很短,但他包含了两个操作,它先读取x的值,在将x的值写入工作内存。读取x的值以及将x的值写入工作内存这两个操作单拿出来都是原子性操作,但是合起来就不是原子性操作了。
语句3包含3个操作: 读取x的值,对x的值进行+1,向工作内存写入新值。
所以,当一个语句包含多个操作时,就不是原子性操作,只有简单的读取和赋值(将数字赋给某个变量)才是原子性操作。
可见性
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果 ,另一个线程马上就可以看到。当一个共享变量被 volatie修饰时,它会保证修改的值立即被更新到主存,所以随其他线程是可见的。当有其他线程需要读取该值时,其他线程会去主存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,并不会立即写入主存,何时被写入主存也是不确定的。当其他线程去读取该值时,此时主存中可能还是原来的旧值,这样就无法保证可见性。
有序性
Java内存模型允许编译器和处理器对指令进行重排序,虽然重排过程不会影响到单线程执行的正确性,但是会影响到多线程并发执行的正确性。这时可以通过 valatie 来保证有序性,除了 voliatie ,也可以通过 synchronized 和 Lock 来保证有序性。syncheonized 和 Lock 保证每个时刻只有一个线程执行同步代码,这相当于让线程顺序执行同步代码,从而保证了有序性。
Volatile 关键字
当一个共享变量被 volatile 修饰之后,其就具备了两个含义,一个是线程修改了变量的值时,变量的新值对其他线程是立即可见的。换句话说,就是不同线程对这个变量进行操作时具有可见性。另一个含义是禁止使用指令重排序。
看下面这个Demo
假设线程1先执行,线程2后执行
//线程1 boolean stop=false; while (!stop){ } //线程2 stop=true;
上面这段代码可以用于中断线程,但是这段代码不一定会将线程中断。虽然概率很小。
为什么说有可能无法中断线程呢? 每个线程在运行时都有私有的工作内存,因此线程1在运行时会将stop变量的值复制一份放在私有的工作内存中。当线程2更改了Stop变量的值后,线程2突然需要去做其他的操作,这时就无法将更改的Stop变量写入到主存中,这样线程1就不会知道线程2对Stop变量进行了更改,因此线程1就会一直循环下去。当Stop用volatile修饰之后,那么情况就变的不同了,当线程2进行修改时,会强制将修改的值立即写入主存,并且会导致线程1的工作内存中变量stop的缓存无效,这样线程1再次读取stop的值时就会去主存读取。
volatile不保证原子性
我们知道 volatile 保证了操作的可见性,但是是否能保证原子性呢?
class A{ public volatile int a=0; public void setA(){ a++; } } public class MyClass{ public static void main(String[] args) { final A a=new A(); for (int i=0;i<10;i++){ new Thread(new Runnable() { @Override public void run() { for (int i=0;i<1000;i++){ a.setA(); } } }).start(); } //如果有子线程就让出资源,保证所有子线程都执行完 while (Thread.activeCount()>2){ Thread.yield(); } System.out.println(a.a); } }
这段代码每次运行,结果都不一致。因为自增不具备原子性,它包括读取变量的原始值,进行+1,写入工作内存。也就是说,自增操作的3个子操作可能会分隔开执行。假如某个时刻变量inc的值为9,线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了。之后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,然后进行加1操作,并把10写入工作内存,最后写入主存。随后线程1接着进行+1操作,因为线程1在此前已经读取了 inc 的值为9,所以不会再去主存读取最新的数值,线程1对 inc 进行+1操作后 inc 的值为10,然后将10 写入工作内存,最后写入主存。两个线程分别对 inc 进行了一次自增操作后,inc 的值只增加了1,因此自增操作不是原子性操作, volatile也无法保证对变量的操作是原子性的。
volatile保证有序性
volatile关键字能禁止指令重排序,因此 volatile 能保证有序性。volatile 关键字禁止指令重排序有两个含义:一个是当程序执行 volatile 变量的操作时,在其前面的操作已经全部执行完毕,并且结果会对后面的操作可见,在其后面的操作还没有进行,在进行指令优化时,在 volatile 变量之前的语句不能在 volatile 变量后面执行;同样,在volatile 变量之后的语句也不能在 volatile变量前面执行。
正确使用volatile 关键字
synchronized关键字可防止多个线程同时执行一段代码,那么这就会很影响程序执行效率。而 volatile 关键字在某些情况下的性能要优于 synchronized 。但是要注意 volatile 关键字是无法替代 synchronized 关键字的,因为 volatile 关键字无法保证操作的原子性。通常来说,使用 volatile 必须具备以下两个条件:
- 对变量的写操作不会依赖于当前值
- 对变量没有包含在具有其他变量的不变式中。
第一个条件就是不能是自增,自减等操作,因为 volatile不保证原子性。
volatile使用场景
1.状态标识
volatile boolean on; public void setOn(){ on=true; } public void Print(){ while(!on){ System.out.println("123"); } }
2.双重检查模式 (DCL)
class Singleton{ private volatile static Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if(instance==null) { synchronized (Singleton.class) { if(instance==null) instance = new Singleton(); } } return instance; } }
在上面的单例模式中,为什么要用valoatile 修饰?
因为 instance=new Singleton(),并非是一个原子操作,事实上在 JVM中这句话大概做了3件事
- 给 instance 分配内存
- 调用 Singletion 的构造函数来初始化成员变量
- 将 instance 对象指向分配的内存空间。(执行完这步 instance 就为 非null 了)
但是JVM 的即时编译器中存在指令重排序的优化,也就是说上面的第二步和第三步顺序是不确定的,一旦2,3,顺序乱了,这时候有另一个线程调用了方法,结果虽然非null,但是未初始化,所以会直接报错。