1.volatile

volatile关键字是一个特征修饰符,确保本条指令不会因编译器的优化而省略。可以li理解为阻止编译器对代码进行优化。

先了解一下原子性(atomicity)和 可见性(visibility)以及有序性

1.1原子性

即一个操作或者一段代码,要么全部执行并且执行过程中不被任何因素打算,要么不执行。

1.2原子操作

1.2.1处理器实现原子操作-(总线锁、缓存锁)

1.处理器会自动保证内存操作的原子性:即从内存中读取或写入一个字节的操作是原子的。处理器处理一个字节的时候,其它处理器不能访问这个字节的内存地址。

2.总线锁保证原子性:即处理器会提供一个LOCK信号,当一个处理器在总线上输出此信号时,会阻塞其它处理器的请求。该处理器独占共享内存。

3.缓存锁保证原子性:因为锁住总线的消耗太大,于是有了缓存锁。即在LOCK期间,处理器缓存的内存区域将会被锁定。其它处理器无法处理该块内存中的数据。

1.2.2Java实现原子操作

Java实现原子操作的方式是锁(悲观锁,乐观锁)和循环CAS

悲观锁:当发生冲突时,即多个线程去争抢共享资源的时候。只有一个线程会获得资源。即获得锁,也称独占锁或互斥锁。其它线程将会被阻塞。直到锁被释放。例如Java中使用synchronized关键字。

乐观锁:CAS就是乐观锁的一种实现方式。当一个线程尝试去获取锁,若是发现锁被占用,会继续尝试获取锁,直到获取成功。

CAS:compare and swap CAS的思想可以这样理解。有三个值,当前内存值A,旧的预期值B,更新值C。当且仅当A=C时,修改A为C,并返回true。否则什么都不做,返回false。即确认更改再赋值

1.3可见性

可见性是指当多个线程操作同一变量时,一个线程修改了变量的值,其它变量能够立刻看到被修改的值。

1.4有序性

即程序执行的顺序按照代码编写的顺序来执行。

int i = 1;
int b = 2;
boolean y = false;

// 语句1
i = 3;

// 语句2
b = 4;

为什么会有有序性的概念呢?

如以上代码,从代码顺序上来看,语句1是排在语句2前面的。但是JVM在执行的时候却不一定能保证语句1在语句2之前执行。这是因为编译器会对代码进行优化,它不保证程序中的各个语句的执行顺序同代码中的相同。但它会保证执行的最终结果是一致的。

是什么会造成代码执行顺序的变化呢?答案是指令重排序。处理器会通过指令重排序来提高程序的运行效率。

指令重排序并不会影响单个线程内代码执行的结果。

boolean y = false;
int i = 1;
// 线程一
i = 4;
y = true;

//  线程2
if (y) {
  i = 5;
}

在线程并发执行的时候,线程2有可能在xian线程1执行y=true之前已经完成执行。从而导致程序的执行没有达到我们的预期。

总结:要想并发程序正确地执行,必须要同时保证原子性、可见性和有序性。只要其中一个没有保证,就有可能保证程序运行不正确。

2.volatile关键字的作用

通过了解关于并发编程的原子性、可见性以及有序性之后。就是禁止指令重排序。使代码按照我们的期望来执行。但是volatile的作用也是有限的。

当多个线程执行同一个方法对一个变量进行自增操作的时候。实际数据结果总是会小于 线程数*执行次数。

这是因为自增操作不是原子操作,而volatile只能保证可见性,并不能保证原子性。

总结:volatile关键字能够用来帮助我们实现线程安全,但是应用十分有限。即:变量的当前值和修改值之间没有约束。

3.使用场景

3.1状态标识

用代码来说明问题

volatile boolean tag = true;

public void stop(){
   tag = false;
}

public void start() {
   while(tag) {
     // TO DO
   }
}

线程1执行start()的时候,我们可以使用其它的线程来调用stop()。由于使用了共同变量,使用volatile关键字保证了其它线程对tag的修改对于线程1可见。能够使线程1及时收到终止信号。

3.2你有可能获得不完整的对象

/* volatile 关键字,解决指令重排序的问题 **/
    private volatile static LazyDoubleCheckSingleton lazy = null;

    private LazyDoubleCheckSingleton () {}

    public static LazyDoubleCheckSingleton getInstance() {
        if (lazy == null) {   //  这里的if是一个避免锁竞争的优化
            synchronized (LazyDoubleCheckSingleton.class) {
                //  这里的if判断防止多个进入外层if条件后实例被覆盖
                if (lazy == null) {
                    lazy = new LazyDoubleCheckSingleton();
                    /* CPU 执行的时候会转换成JVM指令来执行 **/
                    /*    指令重排序 (volatile)
                     *    CPU 执行时第二步和第三步有可能会颠倒
                     */
                    /* 1、分配内存给目标对象(当一个对象 new ,被分配内存) **/
                    /* 2、初始化对象  此时 lazy == null  **/   
                    /* 3、将初始化好的对象和内存地址建立关联(给这个对象赋值) lazy != null 但未完成初始化 **/
                    /* 4、用户初次访问  **/
                }
            }
        }
        return lazy;
    }

通过经典的双重检查锁定示例可以发现,变量lazy在未经volatile关键字修饰的情况下。

当处理器进行指令重排序,即 执行指令顺序变成1》3》2》4,指令3执行完成,指令2未执行的时候。有其他线程来调用单例,将会返回一个不完整的对象。

总结:通过灵活运用volatile和synchronized关键字可以帮助我们写出高校的代码,比如可以使用synchronized控制变量的写操作,使用volatile来控制变量的读操作,从而提高性能。