「并发」浅谈Volatile

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第10天,点击查看活动详情

多线程下变量的不可见性

//        private static volatile int count = 0;
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            try {
                Thread.sleep(1000);
                count++;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("附加线程count=" + count);
        }).start();
        while (true) {
            if (count > 0) {
                System.out.println("变更后主线程中count=" + count);
                break;
            }
        }
    }
复制代码

参考上面代码。在先让线程A执行,再执行其他线程对变量进行修改,就会导致A获取的变量参数不会随着修改而改变。如果增加volatile之后发现,A获取的参数会随着其他线程的修改而被通知到。

总结:线程1获取了变量的值。线程2对变量进行了修改,此时线程1再次获取仍然使用的是原本获取的值。

不可见性的原因

总结:共享变量存储在主内存中,每个线程都有自己的工作内存。主内存的数据修改不会主动同步到试用这个数据的线程独有的工作线程中。

解决不可见性

1、加锁访问共享变量

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(1000);
                count++;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("附加线程count=" + count);
        });
        t.start();
        while (true) {
            synchronized (t) {
                if (count > 0) {
                    System.out.println("变更后主线程中count=" + count);
                    break;
                }
            }
        }
    }
复制代码

锁住的是对成员变量获取的那个线程(不是修改的线程)

原因:一个线程进入synchronized代码块后执行过程

  1. 获取锁对象
  2. 清空工作线程
  3. **拷贝共享变量到工作内存(也就是重新获取,而不是直接使用工作内存中的数据)**所以每次进行while循环都会重新获取一次。
  4. 执行代码
  5. 将修改后的值刷新的主内存
  6. 释放锁

2、对共享变量增加volatile关键字修饰

    private static volatile int count = 0;
//    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(1000);
                count++;
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("附加线程count=" + count);
        });
        t.start();
        while (true) {
            if (count > 0) {
                System.out.println("变更后主线程中count=" + count);
                break;
            }
        }
    }
复制代码

原因:

volatile修饰的变量发生修改并通知到主内存之后,“主内存”会通知所有使用该变量的线程作废该变量在其工作内存中的值

主线程中的值失效之后,就必须得去主内存中重新获取值

Volatile的特性

1、volatile不能保证原子性操作

原子性:一次操作中的所有行为,要么全部成功要么全部失败,且该线程期间状态一致。

    private volatile static Integer count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {

                for (int j = 0; j < 10000; j++) {
                    count++;
                }
            }, String.valueOf(i)).start();
        }
        Thread.sleep(10000);
        System.out.println(count);
    }

export:978024
复制代码

为什么:因为被volatile修饰的count进行自加并不是原子操作,需要先从主内存读取数据到工作内存,然后自增,然后将数据写回主内存。在这个过程中,如果有两个线程同时将数据从100自增到101,然后写入主内存,就会出现问题。结果只增加了一次。即使增加了volatile,也只是保证共享变量在所有线程的可见,而不能保证顺序执行

解决办法:加锁来解决原子性的问题

2、禁止指令重排序

什么是重排序:编译器、指令解释器、操作系统等,为了更好的执行代码提升性能,可能会对代码的指令进行优化排序。如JIT,缓冲区,指令重排序等。

重排序的问题:有些冲排序的情况会导致不按照代码的想法进行执行。比如CPU认为指令顺序调整之后对业务信息和逻辑没有影响。但是在高并发的情况下可能就会出现影响。这个时候如果CPU进行了代码优化,则可能会导致业务问题。(比如a=b;b=1;b=a调整了顺序就会出现结果不一致的问题)

解决重排序:在要修改的变量上增加volatile来避免指令重排序。

猜你喜欢

转载自juejin.im/post/7085324931294035976