locks:volatile关键字
volatile关键字:保证变量的可见性;禁止指令重排序
volatile(不稳定的)如何保证变量的可见性:
在 Java 中,volatile
关键字可以保证变量的可见性,如果我们将变量声明为 volatile
,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取(也就是标志了一个变量“应当存储在主存”)。
volatile
关键字能保证数据的可见性,但不能保证数据的原子性。
public volatile int counter = 0;
volatile不保证volatile++这样的操作具有原子性。
因为“++”属于复合操作。
getAndIncrement()方法中的v++语句被编译成了 七条语句,这属于复合操作。
v++其实相当于:
读v 对v+1; 将原来的v值置为v+1。
volatile保证可见性,当进行++操作的时候,volatile保证第一条指令正确,即读正确。当执行接下来的指令的时候,其他线程可能对v加大了,当将v存回去的时候(即执行putfield指令的时候),可能将一个更小的v同步回主内存去了。所以最终得到的数字就会小于200000.
总结:volatile的读写具有原子性,但是自增操作属于复合操作,因此不具有原子性,所以线程也不安全。
如果你的计算机有多个CPU,每个线程可能会在不同的CPU中运行。这意味着,每个线程都有可能会把变量拷贝到各自CPU的缓存中,如下图所示:
对于非volatile变量,JVM并不保证会从主存中读取数据到CPU缓存,或者将CPU缓存中的数据写到主存中,这会引起一些问题。
如何禁止指令重排序:
如果我们将变量声明为 volatile
,在对这个变量进行读写操作的时候,会通过插入特定的 内存屏障 的方式来禁止指令重排序。
在 Java 中,Unsafe
类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异。理论上来说,通过这个三个方法也可以实现和volatile
禁止重排序一样的效果,只是会麻烦一些:
public native void loadFence(); public native void storeFence(); public native void fullFence();
volatile关键字禁止指令重排序的效果:
双重校验锁实现对象单例(线程安全) :先判断null如果为null再尝试获取锁(提高性能,如果已经创建就不用加锁判断了),获取锁后还不能直接创建,因为之前可能也有判断为null的已经获取过锁并创建对象,所以锁内需要再次检测。两次的if判断与synchronized称为双重校验锁。
public class Singleton { //volatile关键字修饰该对象,禁止指令重排 private volatile static Singleton uniqueInstance; //私有化构造方法 private Singleton() {} public static Singleton getUniqueInstance() { //先判断对象是否已经实例过,没有实例化过才进入加锁代码 //加锁会增加开销,第一次简单的判断(不加锁)能提高执行效率 if (uniqueInstance == null) { //类对象加锁 synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
从上至下进行逐步分析:
1、volatile关键字修饰该类唯一静态变量。
volatile关键字有两个作用,一是保证可见性(这里暂无体现),即其他线程在任何时刻访问到的都是该变量的最新值;二是禁止指令重排,如下:
关于 uniqueInstance = new Singleton(), 这段代码其实是分为三步执行:
①为 uniqueInstance 分配内存空间
②初始化 uniqueInstance
③将对象引用uniqueInstance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。
2、构造方法私有化。确保获取该类的唯一对象实例仅能通过get方法获取。
3、get方法中的两个if判断。因为进入synchronized同步块实现对类加锁会增加锁开销,所以第一个简单的 if 判断(不加锁)能提高执行效率。获取锁后还不能直接创建,因为之前可能也有判断为null的已经获取过锁并创建对象,所以锁内需要再次检测。在此之前,线程需要先获取该类的锁,然后再进入第二次if判断,此时至多只有一个线程可以创建该类对象,保证了线程安全。
至此,该类创建的对象都是唯一确定的。
volatile不能保证对变量的操作是原子性的,利用 synchronized
、Lock
或者AtomicInteger
都可以。
public volatile static int inc = 0; public void increase() { inc++; } //使用synchronized改进 public synchronized void increase() { inc++; }