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不能保证对变量的操作是原子性的,利用 synchronizedLock或者AtomicInteger都可以。

public volatile static int inc = 0;
public void increase() {
    inc++;
}

//使用synchronized改进
public synchronized void increase() {
    inc++;
}

 

posted @ 2023-04-19 22:53  壹索007  阅读(24)  评论(0编辑  收藏  举报