Volatile可见性和原子性

举报
多米诺的古牌 发表于 2021/10/12 17:07:38 2021/10/12
【摘要】 1.Volatile简介Volatile是Java虚拟机提供的轻量级的同步机制。1.1 Volatile理解       volatile变量是Java语言提供的一种的同步机制,即用来确保变量的变更的操作时通知到其他的线程。当使用volatile关键字修饰变量的时候,编译期以及运行期都会注意到这个变量是共享的,因此不会将这个变量上的操作与其他内存操作一起重新排序。       并且volat...

1.Volatile简介

Volatile是Java虚拟机提供的轻量级的同步机制。

1.1 Volatile理解

       volatile变量是Java语言提供的一种的同步机制,即用来确保变量的变更的操作时通知到其他的线程。当使用volatile关键字修饰变量的时候,编译期以及运行期都会注意到这个变量是共享的,因此不会将这个变量上的操作与其他内存操作一起重新排序。

       并且volatile关键字修饰的变量不会被缓存进入寄存器或者其他处理器不可见的地方,在读取volatile关键字修饰的变量时总会返回最新写入的值。

1.2 Volatile的可见性和原子性

1.2.1 保证可见性

        如上面代码中,如果不加volatile关键字,当前线程无法感知主线程中的num的变化,当主线程中tem变量已经变为1的时候,线程A中的tem变量并没有发现tem已经发生了变化,会继续执行while中的循环,一直一直循环下去。。。但是当加了volatile关键词后,tem变量就会对线程A保证了可见性,当发现tem变量发生变化不为0后,就自动跳出循环结束执行任务。

    private volatile static int tem = 0;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (tem == 0){
//                System.out.println("变量num==0,执行"+Thread.currentThread().getName());
            }
        },"线程A").start();
        TimeUnit.SECONDS.sleep(1);
        tem = 1;
        System.out.println(tem);
    }

注:while循环中不能加println打印方法,因为println方法是加锁的,有同步代码块会保证变量修改的可见性,在变量num修改后会立刻刷回到主存中,因此要测试就不能加打印语句。

public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

1.2.2 不保证原子性

1.2.2.1 不保证原子性的小案例

原子性是表示不可分割,执行任务的时候不能被打扰,要么同事成功要么同时失败。

注:yieled()是礼让方法,是让当前线程让出给其他线程来执行,由于java语言中有两个线程是默认执行的,一个是main线程,一个是gc线程,由于Thread.yieled()方法是在主线程中执行的,只要有其它线程存在,就让main线程和gc线程让出cpu给其他线程,这样main线程就不会执行下去,而是礼让后让其他线程执行,等其他线程执行完了后再执行。

private static int tem = 0;
    public static void add(){
        tem++;
    }
    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    add();
//                    System.out.println("for循环中"+Thread.currentThread().getName());
                }
            },"线程"+i).start();
        }
        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+ " "+ tem);
    }

打印结果为:main 1990

可以看出在不做任何处理的情况下多线程自由执行的结果不为20*100=2000;

1.2.2.2 案例源码探析

为什么会出现结果不同的情况?

因为add()方法中只有一行自增的执行语句,我们可以通过javap反编译一下成字节码文件,可以看出add()方法实际上会有有四步:

1.2.2.2.1 获取静态变量,或得值

1.2.2.2.2 常量不用管

1.2.2.2.3 自增,+1的操作

1.2.2.2.4 返回改变后的变量,写回这个变量的值

根据分析我们可以得出num++;这一行自增代码并不是原子性操作,而是分为很多步执行的,所以多线程的情况下会受到影响。而之所以会比理论值小的结果是因为线程回写数据到主存中,覆盖了其他更快执行的结果导致的。

1.2.2.3 那么如何保证结果为我们想要的2000呢?(如何保证原子性)

1.2.2.3.1 可以通过加synchronized关键字或者加lock锁

1.2.2.3.2 使用juc中的原子类来处理(推荐)

由于synchronized关键字或者加lock锁解决更耗费资源,所以我们找到了一种给省资源的方式就是使用juc中的原子类来处理。底层是通过CAS来处理的(直接和操作系统挂钩,在内存中修改值),有很高的的效率。

volatitle和AtomicInteger需要同时使用,不一起使用的话,多运行几次会出现可见性会体现不出来的情况;

//    private volatile static int tem = 0;
    private volatile static AtomicInteger tem = new AtomicInteger();
    public static void add(){
//        tem++;
        tem.getAndIncrement();
    }
    public static void main(String[] args) {
        for (int i = 1; i <= 20; i++) {
            new Thread(()->{
                for (int j = 0; j < 100; j++) {
                    add();
                }
            }).start();
        }

        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName()+ " "+ tem);
    }
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。