多线程系列-volatile(必学)

本文详细探讨了Java中的volatile关键字,包括其保证的可见性和禁止指令重排特性,以及为何不保证原子性。通过代码示例解释了volatile如何在多线程环境下确保变量的最新状态对所有线程可见,以及防止指令重排可能导致的问题。同时,文章通过一个并发场景展示了volatile在原子性上的不足,并提出使用AtomicInteger来解决此类问题。
摘要由CSDN通过智能技术生成

volatile 是多线程中必学的知识点,可以用该修饰词去保证资源的可见性以及禁止指令重排。

volatile 定义:

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

特性:

1.可见性

2.禁止指令重排

但是不保证原子性。下面会逐一进行讲解

可见性:

     首先要了解一个概念,在java内存模型JMM中有说明,变量的值存放在主内存中,线程创建时,jvm会为其分配一个私有的内存工作空间,线程要修改主内存中的变量的值时,

需要先copy一份副本到自己的工作内存中。修改完毕再写回到主内存中。注意:线程之间不能访问其他线程的工作内存。这样就导致,线程1和线程2同时复制了一份变量A的副本到自己的工作内存内,线程1先修改完毕并回写进主内存中,这时线程2还是操作的线程1修改之前的从主内存中复制的数据副本,对线程1的修改并不知情!这就导致了线程2再回写到主内存中时覆盖了线程1的回写数据。那不就乱套了

     可见性是指:线程1在自己的工作空间修改变量A并回写到主程序后,线程2会接收到通知,随后会清除本地的内存空间中的A副本并重新从主内存copy 一份最新的变量副本 重新执行逻辑!

代码示例:首先看一下 线程间不可见 的代码以及效果!
 

public class VolatileDemo {

    private int a = 0;

    private void addTo20(){
        a += 20;
    }


    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        new Thread(()->{
            try {
                //等待一会,等其他线程把变量复制到各自的内存空间
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            volatileDemo.addTo20();
            System.out.println(Thread.currentThread().getName() + "\t run over ,a value:" + volatileDemo.a );
        },"AAA").start();

        while (volatileDemo.a == 0){
            //线程等待其他线程修改完毕后
        }
        System.out.println(Thread.currentThread().getName() + "\t run over,a value : " + volatileDemo.a);
    }
}

这里面 main是一个线程,new Thread 又创建了一个线程 AAA  在线程 AAA中调用 addTo20方法 修改 变量a的值为20.在main线程中while等待 变量a值的变化 值改变后会输出最后一条打印消息!

执行结果:main程序一直在循环中,并没有检测到变量a的值已经被其他线程改变了。

下面,我们加上volatile 修饰后,再次执行main方法

private volatile int a = 0;

执行结果:main方法的最后一行成功执行并打印出了a.value = 20

以上代码证明了 volatile的特性1:可见性。

2.禁止指令重排。

   首先了解下 指令重排的概念  ,指令重排 是指编辑器和cpu有时会为了优化程序的执行效率,调整程序的执行顺序,指令重排只可能发生在毫无关系的指令之间, 如果指令之间存在依赖关系, 则不会重排.

举例:

int a = 0;//指令1

int b = 0;//指令2

a +=b;//指令3

按照程序的编写顺序 是 1,2,3顺序执行,但是在指令重排后,可能会 2,1,3这样的顺序执行,因为指令1和2毫无关系,先执行谁都可以。指令3由于使用到了指令1和指令2的值 所以只会等待 指令1和2执行完毕后再执行指令3

按理说是做的优化,为什么还要禁止呢?这里我们拿一个单例的创建代码举个例子:

class MyData{
    private static MyData myData;
    private MyData(){
        System.out.println("MyData init");
    }

    /**
     * 获取单例bean的方法
     * @return
     */
    public static MyData getInstance(){
        if(myData == null){
            synchronized (MyData.class){
                if(myData == null){
                    myData  = new MyData();
                }
            }
        }
        return myData;
    }
}

上面的代码就是一个获取单例Bean的方法!基本看不出什么问题吧!但是上述代码同样存在着指令重排的风险。虽然几率很小!

原因出在了 new MyData()这一句代码上 这句话写代码是一句话,但是到了底层执行时 会分成多个指令,下面是我自己的理解,可能有偏差,欢迎指正!

1.开辟所需内存空间

2.初始化对象,数据创建到开辟好的内存空间中

3.变量引用指向开辟的内存空间地址

正常来说 1,2,3顺序执行没啥问题,如果指令重排后可能会出现 1,3,2的执行顺序(2和3互不影响,也不存在依赖关系,所以2和3可能会被重排)。单线程下没有什么问题!但是多线程下,

线程1进来判断myData对象不存在 则进去创建对象 执行的顺序是 1,3,2 当执行完3后 myData已经有引用了。虽然指向的内存空间中还未创建数据!

这时另一个线程被分配了时间片执行 if(myData == myData)这句就会返回false,然后return了一个 空内存的对象,后续的一些基于该对象的操作就会出现执行错误!

解决方法:用 volatile修饰变量就可以禁止 指令被重排。如下

private static volatile MyData myData;

第三点:就是volatile不保证原子性。

虽然提供了可见性。但是由于线程执行过快,线程1修改工作内存中的变量值并且写入到主内存中,其他线程在还未接收到通知就执行了写入操作,就会导致线程1的写入被覆盖了。下面代码举例:

public class VolatileDemo {


    private volatile int a = 0;

    private void addTo20(){
        a += 20;
    }

    private void addAdd(){
        a++;
    }


    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        for (int i = 0;i<10;i++){
            new Thread(()->{
                for(int j = 0;j<1000;j++){
                    volatileDemo.addAdd();
                }
            }).start();
        }
        //这里等待线程全部执行完毕,大于2是因为 有一个main线程和一个gc的线程
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println("a value:" + volatileDemo.a);
    }
}

启动了10个线程 ,每个线程执行1000次 addAdd 方法 相当于执行了 1000次+1 ,我们期望结果 a = 10000.但实际执行后

会发现 小于10000 ,原因就是有些线程的++后写入到主内存后,其他线程未及时被通知到主内存中的a修改就执行了写入操作。导致之前的线程写入被覆盖了!

解决方法:可以使用  java.util.concurrent.atomic 包下的  AtomicInteger 对象 

Atomic 是原子性的意思 加上 Integer 就是 原子性的 Integer。具体的api可参考 javaApi文档

完善后的代码

public class VolatileDemo {


    private volatile int a = 0;

    private AtomicInteger atomicInteger = new AtomicInteger();

    private void addTo20(){
        a += 20;
    }

    private void addAdd(){
        a++;
    }
    private void myIncrementAndGet(){
        atomicInteger.incrementAndGet();
    }


    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        for (int i = 0;i<10;i++){
            new Thread(()->{
                for(int j = 0;j<1000;j++){
                    volatileDemo.addAdd();
                    volatileDemo.myIncrementAndGet();
                }
            }).start();
        }
        //这里等待线程全部执行完毕,大于2是因为 有一个main线程和一个gc的线程
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        System.out.println("a value:" + volatileDemo.a);
        System.out.println("atomicInteger value:" + volatileDemo.atomicInteger);
    }
}
增加成员变量     private AtomicInteger atomicInteger = new AtomicInteger();
增加 atomicInteger 变量的++方法 myIncrementAndGet
在线程中调用 volatileDemo.myIncrementAndGet();每次+1 
预期结果为 atomicInteger 的值为10000
执行结果:

 完毕!!!!

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值