Java关键字volatile全面解析和实例讲解

61 阅读7分钟

简介

volatile是JVM提供的一种轻量级的同步机制。相比于synchronized关键字volatile更==轻量级==(乞丐版的synchronized),因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

在并发编程中,我们通常要保证可见性原子性有序性可见性:当多个线程共享同一变量时,若其中一个线程对该共享变量进行了修改,那么这个修改对其他线程是立即可见的。 原子性:一个或多个操作为一个整体,要么都执行且不会受到任何因素的干扰而中断,要么都不执行。 有序性:程序执行的顺序按照代码的先后顺序执行。

==但是volatile只保证了可见性和有序性。==

java内存模型JMM介绍——可见性

JVM运行程序的实体是==线程==,而每个线程创建时==由JVM分配各自的工作内存==。线程的工作内存是==各自私有的数据区域==,互相各不影响。 ==java内存模型中所有的变量是存储在主内存中的,主内存是线程共享的区域==,但是线程对变量的操作(读取赋值)必须在==工作内存==中进行, 当多个线程访问同一个变量(放在主存中)的时候,并不是直接在主存中操作变量,而是==将此对象分别拷贝到每一个线程各自的工作内存中==, 当其中一个线程操作了变量之后,需要将修改后的对象(也就是最新值)==重新写回主内存==,也要将最新值同步给其他的线程。(可见性:让其他的线程可以看到。) ==线程之间的通信(传值)必须通过主内存来完成==。如下图: JMM工作原理图

1.线程解锁前,必须把==共享变量刷回主内存== 2.线程解锁前,必须读取主存的==最新值==到自己的工作内存 3.加锁解锁是==同一把锁==

volatile——可见性

首先先看一段代码

public class volatileTest {
    public static void main(String[] args) {

        // 资源类
        myNumber myNumber = new myNumber();
        // 声明一个线程,改变number的值
        new Thread(() ->
        {
            System.out.println(Thread.currentThread().getName() + "   线程启动……");
            try {
                // 让线程等待3秒,保证main线程执行到while循环
                TimeUnit.SECONDS.sleep(3);
            } catch (Exception e) {
                e.printStackTrace();
            }
            myNumber.addTo10();
            System.out.println(Thread.currentThread().getName() + "   执行后的number:" + myNumber.number);
        }, "T1").start();


        while (myNumber.number == 0) {
            // main线程一直在等待执行,直到number不在等于0
        }
        System.out.println(Thread.currentThread().getName() + "线程执行结束!执行后的number:" + myNumber.number);
    }


}

class myNumber {
    int number = 0;

    // 将number的值变为10
    public void addTo10() {
        this.number = 10;
    }
}

执行结果为: 是不是和自己想的并不一样呢?最后的输出一直都没有打印,说明main拿到的number 一直是0,上边的程序是两个线程T1和main修改共享变量number,我们知道共享变量实际是存在主存中的,而两个线程是在自己的工作内存操作的只是number的副本,线程间变量的值的传递需要通过主内存中转来完成。当T1修改完成之后由于线程的工作不存相互独立导致修改后的number不可见,所以mian线程才会一直处于循环中。

那么上边的问题应该怎么解决呢? 当然是我们今天的主角:volatile! 只需要将number用volatile修饰即可:

class myNumber {
    volatile int number = 0;

    // 将number的值变为10
    public void addTo10() {
        this.number = 10;
    }
}

执行结果: 在这里插入图片描述 volatile修饰的变量可以让其他的线程迅速写回主存,通知其他线程变量的最新值。

volatile——不保证原子性

例如a++这种,其实是经历了三步操作:获取,增加,赋值,在多线程的情况下,这种操作会导致丢值的情况。所以以下的代码不能到10000.

public class volatileTest {
    public static void main(String[] args) {

        // 资源类
        myNumber myNumber = new myNumber();

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    myNumber.plusOne();
                }
            },String.valueOf(i)).start();
        }

        if (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "线程执行结束!执行后的number:" + myNumber.number);
    }


}

class myNumber {
    volatile int number = 0;
    
    // 将number的值+1
    public void plusOne() {
        this.number ++;
    }
}

volatile——不保证原子性问题解决

1.使用 synchronized修饰方法

class myNumber {
    volatile int number = 0;

    // 将number的值+1
    public synchronized void plusOne() {
        this.number ++;
    }
}

但是synchronized是一种重量级的锁,所以不建议。

2.采用java并发包中的原子操作类,原子操作类是通过CAS循环的方式来保证其原子性的

class myNumber {
//    volatile int number = 0;
    AtomicInteger integer = new AtomicInteger();

//    // 将number的值+1
//    public void plusOne() {
//        this.number ++;
//    }

    // 将integer的值+1
    public void atmicplusOne() {
        integer.getAndIncrement();
    }
}

volatile——指令重排

计算机在执行程序时,为了提高性能,编译器和处理器尝尝会对指令重排,一般分为一下三种:

在这里插入图片描述单线程环境里边确保程序最终执行结果和代码顺序执行的结果一致。

处理器在进行重排序时必须要考虑指令之间的==数据依赖性==。

多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

先看一段代码:

 		int a = 1; // 1
        int b = 2; // 2
        a = a + 1; // 3
        b = a * a; // 4
  1. 那么程序按照1234的顺序可以正常输出,不影响我们最初的逻辑。
  2. 按照2134,1324顺序和1234也是没有违背最初的逻辑。
  3. 但是如果说4123的顺序可不可以呢?肯定是不行的,此时a,b变量都没声明,没有办法进行使用(依赖性)。

也就是说在多线程的情况下,2的情况属于指令重排的,3的情况是不会指令重排的,因为会影响原有的逻辑和输出结果。

那么为什么要禁止指令重排呢?下边在看一段代码:

	int a = 1;
    boolean flag = false;

    public void method1() {
        flag = true;
        a = 1;
    }

    public void method2() {
        while (flag) {
            a = a + 1;
        }
    }

上边这段代码中 int a = 1; boolean flag = false;

和方法1中的

flag = true; a = 1; 都仅仅是变量的声明,赋值,也就是说会导致指令重排,但是调用方法2的时候就会出现变量a还没有重新赋值导致了结果错误,所以有时候要禁止指令重排。

双重锁式单例模式

public class Singleton {
    // volatile 保证可见性和禁止指令重排序
    private static volatile Singleton singleton;

    public static Singleton getInstance() {
        // 第一次检查
        if (singleton == null) {
          // 同步代码块
          synchronized(this.getClass()) {
              // 第二次检查
              if (singleton == null) {
                    // 对象的实例化是一个非原子性操作
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

上面代码中, new Singleton() 是一个非原子性操作,对象实例化分为三步操作: (1)分配内存空间, (2)初始化实例, (3)返回内存地址给引用。

所以,在使用构造器创建对象时,编译器可能会进行指令重排序。假设线程 A 在执行创建对象时,(2)和(3)进行了重排序,如果线程 B 在线程 A 执行(3)时拿到了引用地址,并在第一个检查中判断 singleton != null 了,但此时线程 B 拿到的不是一个完整的对象,在使用对象进行操作时就会出现问题。

所以,这里使用 volatile 修饰 singleton 变量,就是为了禁止在实例化对象时进行指令重排序。

总结

提示:这里对文章进行总结: 例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。