Java volatile关键字

先了解一下JMM:

JMM是Java的内存模型,是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这种规范定义了程序中的各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。(好比十二生肖中的龙,有这个概念,但不真实存在)

JMM要求保证可见性、原子性、有序性,volatile可以保证其中的两个,本篇文章具体验证volatile的可见性,不原子性和禁重排,同时解决volatile的不保证原子性,让代码具有原子性。

JVM运行程序的实体是线程,每个线程在被创建时JVM都会为其创建一个自己私有的工作内存。而Java内存模型规定所有的变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但是线程对这些变量的操作只能在自己的工作内存中进行,不能直接操作主内存中的变量,要先将变量从主内存中拷贝到线程自己的工作内存中再对其进行操作,操作完成后再将操作后的变量写回主内存当中,因此不同的线程也无法访问对方的工作内存,线程间的通信必须通过主内存来完成。

Java volatile关键字_第1张图片

 

volatile是java虚拟机提供的轻量级同步机制。其三大特性为保证可见性、不保证原子性、禁止指令重排。

1.可见性。

   如果此时A线程和B线程同时获取主内存中的同一个变量,之后A线程修改了这个变量,但是此时B线程并不知道A线程已经对数据进行了修改,所以要具有可见性让线程之间进行通讯。当线程A修改完以后线程B也能知道此时该变量的值已经变为A修改后的数据,实现可见性。具体验证可见性的代码如下:

package volatiledemo;

//实体类
public class Mydata {

    //成绩 = 0
    int grade = 0 ;

    //调用次方法成绩会变为60
    public void mydata(){
        this.grade = 60;
    }

}
package volatiledemo;


import java.util.concurrent.TimeUnit;

public class VolatileDemo {

    public static void main(String[] args) {
        Mydata mydata = new Mydata();
        new Thread(() ->{
            /*
             * Thread.currentThread().getName()
             * 返回正在被执行的线程的名称
             */
            System.out.println(Thread.currentThread().getName()+"come in grade value:"+mydata.grade);
            try {
                /*
                 * 1.TimeUnit.SECONDS.sleep()这个方法可以精确到任意时间,
                 * 指定DAYS、HOURS、MINUTES,SECONDS、MILLISECONDS和NANOSECONDS
                 * 2.Thread.sleep()参数只能是毫秒,只可以精确到毫秒数
                 */
                TimeUnit.SECONDS.sleep(3);
                //Thread.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            mydata.mydata();
            //执行后的结果信息
            System.out.println(Thread.currentThread().getName()+"updated grade value:"+mydata.grade);
        },"Teacher").start();
        //此时如果main线程执行到这里,进入死循环,代表没有可见性。
        while (mydata.grade == 0){

        }
        //此时代表main线程已拿到Teacher线程修改后的结果
        System.out.println(Thread.currentThread().getName()+"mission is over:"+mydata.grade);
    }
}

执行以上代码可以看到代码进入死循环,代表没有可见性,B线程并不知道A线程已经将变量修改。如图:

Java volatile关键字_第2张图片

此时我们只需要将实体类中的变量加上volatile关键字即可实现可见性。

package volatiledemo;

//实体类
public class Mydata {

    //成绩 = 0
    volatile int grade = 0 ;

    //调用次方法成绩会变为60
    public void mydata(){
        this.grade = 60;
    }

}

执行结果如图:

Java volatile关键字_第3张图片

此时B线程已经知道A线程修改了变量的值,实现了可见性。 

2.不保证原子性

volatile不保证原子性,测试如下:

package volatiledemo;

import java.util.concurrent.atomic.AtomicInteger;

public class Mydata {

    volatile int grade = 0 ;

    public void mydata(){
        this.grade = 60;
    }

    public void addadd(){
        grade++;
    }
}

可以看到,此时的grade是加了volatile的

package volatiledemo;

public class VolatileDemo {
    public static void main(String[] args) {
        Mydata mydata = new Mydata();
        //循环创建20个线程,每个线程执行addadd方法一千次
        for (int i = 1; i <=20 ; i++) {
            new Thread(() ->{
                for (int j = 1; j <=1000 ; j++) {
                    mydata.addadd();
                }
            },String.valueOf(i)).start();
        }
        /**Thread.activeCount()
         * 此方法返回活动线程的当前线程的线程组中的数量。
         */
        //如果当前线程大于2,代表除了当前main线程还有其他后台GC线程
        while (Thread.activeCount()>2){
            /**
             * Thread.yield()
             * 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)
             */
            //当还有其他线程时,main线程进行礼让,让其他线程先执行
            Thread.yield();
        }
        //输出20个线程执行完毕后grade的最终值
        System.out.println(Thread.currentThread().getName()+"final value:"+mydata.grade);
    }
}

执行以上方法,可得到结果如图

Java volatile关键字_第4张图片

Java volatile关键字_第5张图片

 两次执行结果均未实现想要效果

原子性即为保证完整性,不可分割,即某个线程在做操作时,是不能被加塞或者分割的,需要整体完整,要么同时成功,要么同时失败。而现在出现的情况,是因为线程之间出现了覆盖,A线程和B、C线程同时拿到数据进行加加操作,当A写回主内存时B或C并没来得及拿到更改后的共享数据,可能会出现被覆盖的效果,所以说volatile并不保证原子性。

volatile只是保证了可见性,可以让其他线程看到某一线程修改后的数据。

例如考试,A和B抄小马的答案,此时的小马就代表主内存,但是离交卷还有几秒,B反复思考一道题最终更改为了正确答案并告诉小马,你这个错了,快改过来,此时小马听后立马改掉了答案,A看到了小马更改了答案,因为A和B坐在不同的方向,就像线程一样不能交互,只能通过主内存的共享数据进行交互,B告诉小马时A并不知道,直到小马更改完答案后A才知道,但此时卷子已经提交,A没有时间修改。在线程中就导致了覆盖的现象。A,B加完1给主线程,B给主线程回写完了,但是此刻A也同时写回了,主内存值本应为2,但是被覆盖为1。

解决方法:

1.加synchronized同步锁。(但是此处使用synchronized显得太重了,杀鸡用牛刀!)

2.使用juc下的AtomicInteger可以保证原子性(CAS自旋锁),代码如下:

package volatiledemo;

import java.util.concurrent.atomic.AtomicInteger;

public class Mydata {

    volatile int grade = 0 ;

    public void mydata(){
        this.grade = 60;
    }

    public void addadd(){
        grade++;
    }
    //new AtomicInteger(); ()里不写默认为0,相当于现在atomicInteger = 0
    AtomicInteger atomicInteger = new AtomicInteger();
    public void addMyAtomic(){
        //等同于++  带原子性的++
        atomicInteger.getAndIncrement();
    }
}
package volatiledemo;

public class VolatileDemo {
    public static void main(String[] args) {
        Mydata mydata = new Mydata();
        //循环创建20个线程,每个线程执行addadd方法一千次
        for (int i = 1; i <=20 ; i++) {
            new Thread(() ->{
                for (int j = 1; j <=1000 ; j++) {
                    mydata.addadd();
                    mydata.addMyAtomic();
                }
            },String.valueOf(i)).start();
        }
        /**Thread.activeCount()
         * 此方法返回活动线程的当前线程的线程组中的数量。
         */
        //如果当前线程大于2,代表除了当前main线程还有其他后台GC线程
        while (Thread.activeCount()>2){
            /**
             * Thread.yield()
             * 使当前线程从执行状态(运行状态)变为可执行态(就绪状态)
             */
            //当还有其他线程时,main线程进行礼让,让其他线程先执行
            Thread.yield();
        }
        //输出20个线程执行完毕后grade的最终值
        System.out.println(Thread.currentThread().getName()+"grade final value:"+mydata.grade);
        System.out.println(Thread.currentThread().getName()+"atomicInteger final value:"+mydata.atomicInteger);
    }
}

结果如下:

Java volatile关键字_第6张图片

 3.禁止指令重排

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。

计算机在执行程序时,为了提高性能,编译器和处理器通常会对指令做出重排:

 现有如下代码

int a = 0;//语句1
int b = 0;//语句2
a = a+3;//语句3
b = a+a;//语句4

如果是在单线程环境里面程序的最终执行顺序和代码顺序的结果是一致的,即为1234,但是在多线程环境下,可能会出现的顺序为1234,2134,1324,因为经过了指令的重排。但是处理器在进行重排的时候必须要考虑指令之间的数据依赖性,代码的执行顺序不能将还未声明的变量作为第一执行语句,要先声明再使用。

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

class MyData1{
    int a = 0;
    Boolean b = false;
    public void Mydata1(){
        a = 1;//语句1
        b = true;//语句2
    }
    public void Mydata2(){
        if (b = true){
            a = a + 2;
        }
        System.out.println(a);
    }
}

以上代码在多线程中按顺序正常执行结果应该为a = 3,b = true。但是在存在指令重排的情况下,结果可能为情况1:a = 3;b =true,情况2:b = true,a = 2的情况,因为指令会做出重排,a和b也不存在数据依赖性,所以Mydata1里面的语句2可能会先执行,但是此时线程2立马抢到资源,拿到的b为true,但是此时Mydata1中的语句1还未执行,就会导致a的结果可能会为0+2=2。

所以,在一些场景下,我们不需要指令去做出重排时,可以使用volatile关键字。通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。

内存屏障:

又称内存栅栏,是一个CPU指令,作用为:

1.保证特定操作的执行顺序。

插入内存屏障禁止在内存屏障前后的指令执行重排序优化。

2.保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。

强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

对volatile变量进行写操作时,会在写操作后加入一条store屏障质量,将工作内存中的共享变量值刷新回到主内存。进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量 。

你可能感兴趣的:(java,开发语言)