java多线程(三) volatile关键字

一、用图说话

java多线程(三) volatile关键字_第1张图片
在这里插入图片描述
  • 问题思考:
    为什么需要volatile这个关键字。通过上图我们可以看出,cpu为了获得更快的速度,是允
    许线程对于共享内存中的共享变量进行私有拷贝的,也就是说java使用主内存来保存变量
    的值,而每个线程有自己独立的工作内存,这样就造成了,线程对自己拷贝的那一份值进
    行一些操作的时候,会造成工作内存变量拷贝的值和主内存中不一致的情况。

二、情景在线

  • 当执行i=10++操作时,干了什么事呢?
    底层第一步需要先load i到线程的工作内存中,在对i进行10++操作,最后进行赋值,最后
    刷入主内存中。
    1、设想我们准备两根线程执行这个操作,我们希望,最后得到的值为12,因为想当然第
    一次,对10进行了加一操作,然后对产生的结果11在次加一操作,不就是12吗?
    2、但是会存在这种情况,线程一和线程二同时将i load到线程的工作内存中,线程一执行
    了对i的加一操作,然后将结果11写入主存中,但是线程二现在的工作内存中i的值还是为
    10,继续执行加一操作,得到的值还是为11,继续同步到主存中,最后尴尬的是结果还是
    11,这是因为每个线程的工作内存对其余的为不可见。

三、多线程的三大特性。

  • 可见性
    对于volatile关键字,今天主要讨论可见性和有序性这个特点,正因为java内存模型的设计,对于不同线程的
    工作内存,是属于线程间的私有内存,是不可见的,那么我们如何确保,一个线程修改了共享变量
    中的值以后,其他线程能立刻看见。
    1、java是如何解决这个问题的?
        java提供了volatile这个关键字,来保证可见性问题,当一个变量被这个关键字修饰时,它会保证
        修改的值会立马刷入主存中,其他线程去读取这个值的时候,会拿到这个值的最新值,其实我个
        人猜想,java是在主存中加了一块内存屏障,当线程对这个操作未完成的时候,其他线程访问不
        了这个块内存,在外等待。
        
    
  • 程序例子
    public  static int i=20;

    public static void main(String[] args) {


        Thread t1=new Thread(()->{
            i++;
        });
        t1.start();
        Thread t2=new Thread(()->{
            i++;
            System.out.println(Thread.currentThread().getId()+":"+i);
        });
        t2.start();
        System.out.println("---------"+i);
    }
}

多执行几次会发现输出的结果和预想的不一样。但是对i加上了volatile关键字后,在看看结果。
  • 有序性:
    在代码执行的过程中,代码执行的顺序是否是按照代码编写的先后顺序执行的呢?
    答案是不一定的,cpu为了提高运行的效率,会对指令进行重排序,他最终会保证数据
    最后的结果为正确。
    直接上代码:
   int i=0;
   boolean flag=false;
   i=10;    //语句1
   flag=true; //语句2
   //对于语句1和语句2执行的顺序会1->2吗?其实不一定,对于语句1和语句2谁先之执行对程序其实没
   多大影响。
   ** 但是注意的是:cpu不是,不会瞎搞指令重排,对于有关联的操作,比如操作A,需要操作B的结果,
   那么操作A就不会在操作B的前面。
  • 思考指令重排会对多线程操作造成什么影响呢?
很容易理解,对于单线程来说,其实指令的重排并没有什么影响。
package com.pjw.Thread.vo;

public class Reorder {



    static  boolean flag=false;
    public static void init(){
        System.out.println("初始化操作完成了");
    }

    public static void main(String[] args) {



        Thread t1=new Thread(()->{
            init(); //1⃣️
            flag=true; //2⃣️
        });




        Thread t2=new Thread(()->{
            while (flag){
                try {
                    System.out.println("进入了方法");
                    Thread.sleep(50000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            System.out.println("没进入while循环");

        });

        t1.start();
        t2.start();

     //运行以上的代码,在多次测试后会发生两种情况,对于1,2两句指令,没有特殊的关联,所以谁先运行
     你我也不知道,只有cpu知道,那么对于正常的流程,语句1先运行进行初始化,然后对flag进行赋值,是
     正确的,但是如果语句2先运行,那么结果是不是就错误了呢?
     我们加上volatile关键字以后在看一看效果。

   //所以得出结论,有序性对于多线程程序也是需要的,没法保证有序性会造成程序的错误。
    }
}
  • java中的有序性:
    在java中,允许出现指令重排的,对于多线程来说指令重排会导致一些不可预见的错误。
    java中可以通过volatile关键字来禁止指令重排或使用一些锁机制来保证有序性,另外最近
    在翻阅jsr规范的时候了解到java内存模型保证了一些有序性,通常叫做happens-before原
介绍一下:
   Two actions can be ordered by a happens-before relationship. 
   If one actionhappens-before another, then the first is visible to and ordered before the second.
   上述为概论:简单的翻译一下,就是两个动作如果符合happens-before原则,第一动作必先发生在第二个
   动作以前,并且对其可见。
   1、If x and y are actions of the same thread and x comes before y in program order,then hb(x, y);
     (对于同一个线程中两个动作,写在前面的必先发生在后面的以前。)
   2、If hb(x, y) and hb(y, z), then hb(x, z)
       (传递规则,如果x先于y,y先于z,则x先于z)
   3、An unlock on a monitor happens-before every subsequent lock on that monitor
   (一个锁释放操作先于后面对同一把锁的获得操作)
   4、A write to a volatile field (§8.3.1.4) happens-before every subsequent read ofthat field.
   (一个volatile的写操作先于读操作。)
   5、A call to start() on a thread happens-before any actions in the started thread.
   (一个线程的start方法,先于这个线程的任何方法)
   6、All actions in a thread happen-before any other thread successfully returns froma join() on that thread.
   (线程中的所有操作先于这个线程的中止检测,比如说join()方法或islive()方法)
   7、There is a happens-before edge from the end of a constructor of an object to thestart of a finalizer
          for that object.
         (线程的初始化操作先于他的回收操作,想想如果我还没初始化就被干掉,我得郁闷死啊)

解释一下第一条:我们刚才的代码不就是同一个线程的中两个动作吗?还是指令重排了啊,因为第一个只保证
单线程的情况下,对于多线程是不保证的。

四、作用

  • volatile关键字到底有什么作用?
    1、保证了可见性,一旦一个共享变量被其修饰,保证一个线程操作以后,会立马刷新到内存
    中去,保证其他线程可以看见。
    2、禁止了指令的重排;(两层意思)
  • 在程序读到这个关键字的时候,在其前面的操作更改肯定全部完成了,且结果对后面必须可见。
  • 在进行指令优化时,不能将volatile操作的语句放在后面执行,也不能把volatile的后面语句放在前面执行。一个内存屏障的作用吧我感觉。
    3、思考对于线程的三大特性原子性,volatile可以保证吗?先看下面的代码?
package com.pjw.Thread.vo;

public class AtomicDemo {

    public volatile int inc=0;

    public void increase(){
        inc++;
    }

    public static void main(String[] args) {
        final  AtomicDemo demo=new AtomicDemo();

        for(int i=0;i<10;i++) {

            new Thread() {
                @Override
                public void run() {
                    for (int i = 0; i < 100; i++) {
                        demo.increase();
                    }
                };
            }.start();
        }


        System.out.println(demo.inc);
//实验发现,每次输出的结果都不到1000,是因为volatile只保证了可见性,没有保证原子性。
inc++不是原子操作,如果线程一只是load完inc以后,并没有++操作,就被等待,其实对于其他线程
拿到的数据还是线程一的原始数据,没有达到我们需要的效果。其实这样可以使用atomic包或锁,保证原子操作下一节介绍CAS等操作。
    }
}

五、解析以前单例模式的困惑。

  • 为什么单例模式会使用volatile修饰instance,原因是防止指令重排;
    instance=new Instance()不是一个原子操作。
    1、new关键字给instance在heap中分配内存;
    2、调用构造函数进行初始化;
    3、将引用指向对象。
    但是二三操作的顺序我们是不能保证的,一旦3先执行于2,这时候有线程过来取,得到的
    instance不是null,但是还没有初始化,从而导致调用报错。

六、总结

细细研究还是很多东西的,路漫漫其修远兮,吾将上下而求索。

你可能感兴趣的:(java多线程(三) volatile关键字)