对于volatile的理解

​volatile是java虚拟机提供的轻量级的同步机制。

它有三大特性:

  • 保证可见性

  • 不保证原子性

  • 禁止指令重排

1、保证可见性

JVM运行程序的实体是线程,每个线程在创建时JVM都会为其创建一个工作内存(有些地方称为栈空间)。工作内存是每个线程的私有数据区,而java内存模型(JMM)中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问。

 

但问题是:线程对变量的操作(读、取、赋值等)必须在工作内存中进行:首先将变量从主内存拷贝到自己的工作内存,然后再对变量进行操作,在操作完成之后再将变量写回到主内存中。

 

那么什么是可见性呢?可见性指的是当主内存区域中的值被某个线程修改后,会马上通知其它线程,其它线程会马上知道并得到修改后的值

 

可见性代码验证:

public class VolatileDemo {    public static void main(String[] args) {        Data data = new Data();        new Thread(()->{            System.out.println(Thread.currentThread().getName() + "\t come in");            //暂停一下A线程,让main线程先执行            try {                TimeUnit.SECONDS.sleep(3);            } catch (InterruptedException e) {                e.printStackTrace();            }            data.change();            System.out.println(Thread.currentThread().getName() + "\t 修改后的值为"+data.number);        },"A").start();        while (data.number==0){        }        System.out.println(Thread.currentThread().getName() + "\t 通知别的线程成功,保证可见性");    }}class Data{    //资源类    int number = 0;    public void change(){        number = 10;    }}

运行结果:

这说明:没有加volatile关键字的话,程序在多线程的情况下是不保证可见性的。加上volatile之后:

将int number = 0;修改为volatile int number = 0;

 

2、不保证原子性

原子性:不可分割,完整性。某个线程正在做某个具体业务时,中间不可以被加塞或被分割,要么同时成功,要么同时失败。

 

代码验证:

public class Test01 {    public static void main(String[] args) {        MyData myData = new MyData();        for (int i = 1; i <=20 ; i++) {            new Thread(()->{                for (int j = 1; j <= 1000; j++) {                    myData.addPlusPlus();                }            },String.valueOf(i)).start();        }        //需要等待上面20个线程全部计算完成后,再用main线程取得最终的结果值看是多少        while (Thread.activeCount() > 2){            Thread.yield();        }        System.out.println(Thread.currentThread().getName() + "\t int类型最后结果"+myData.number);    }}class MyData{    volatile int number = 0;    public void addPlusPlus(){        number++;    }}

上面number加了volatile,我们的期望结果是20000,但实际运行结果:

那么我们该如何解决原子性的问题呢?

  • 加synchroized

运行结果:

  • 使用JUC下的AtomicInteger

代码示例:

public class Test01 {    public static void main(String[] args) {        MyData myData = new MyData();        for (int i = 1; i <=20 ; i++) {            new Thread(()->{                for (int j = 1; j <= 1000; j++) {                    myData.addMyAtomic();                }            },String.valueOf(i)).start();        }        //需要等待上面20个线程全部计算完成后,再用main线程取得最终的结果值看是多少        while (Thread.activeCount() > 2){            Thread.yield();        }        System.out.println(Thread.currentThread().getName() + " AtomicInteger 类型的结果:"+myData.atomicInteger);    }}class MyData{    AtomicInteger atomicInteger = new AtomicInteger();    public void addMyAtomic(){        atomicInteger.getAndIncrement();    }}

运行结果:

 

3、指令重排

什么是指令重排呢?

 

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

源代码-->编译器优化的重排-->指令并行的重排-->内存系统的重排-->最终执行的指令

 

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

 

单线程情况下,最终执行结果和代码顺序是一致的。

 

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

 

public class Sort(){  int x = 1;  int y = 2;  x = x+3;  y = x*x;}

单线程执行顺序:1    2    3    4

多线程可能出现很多情况:

  • 2    1    3    4

  • 1    3    2    4

上述情况便是指令重排,即内部执行顺序和我们代码的顺序不一样。

 

但是指令重排也是有限制的,不会出现下面的顺序:

  • 4    3    2    1

因为处理器在进行指令重排的时候,必须考虑到指令之间的数据依赖性。

 

指令重排代码示例:

public class ReSortSeqDemo {    int a = 0;    boolean flag = false;    public void method01(){        a = 1;        flag = true;    }    //多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测    public void method02(){        if (flag){            a = a+5;            System.out.println("retValue:"+a);        }    }}

我们顺序调用method01()和method02(),单线程下:

a = 1;flag = true;a = a+5;

但在多线程环境下会出现:

flag = true;a = a+5;System.out.println("retValue:"+a);a = 1;

 

孤独,是给我们思考的时间,在一个人的日子里,我们要做的只有一件事,那就是把自己变得优秀。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值