• JUC并发编程——Volatile详解(基于狂神说的学习笔记)


    Volatile

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

    1、保证可见性

    public class JMMDemo {
        // 在num前添加关键字volatile,保证num在所有线程可见,即修改就被通知
        private volatile static int num = 0;
        public static void main(String[] args) throws InterruptedException {// 主线程
    
            new Thread(()->{// 线程1
                while(num == 0){
    
                }
            }).start();
            TimeUnit.SECONDS.sleep(1);
    
            num = 1;
            System.out.println(num);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2、不保证原子性

    原子性:不可分割的操作

    线程A在执行任务的时候,不能被打扰,也不能被分割,要么同时成功,要么同时失败

    package Volatile;
    
    // 验证 volatile不保证原子性
    public class vDemo02 {
    
        private volatile static int num = 0;
        public static void add(){
            num++;
        }
        public static void main(String[] args) {
            for (int i = 1; i <= 20; i++) {
                new Thread(()->{
                    for (int j  = 0; j < 1000; j++) {
                        add();
                    }
                }).start();
            }
    
            while(Thread.activeCount()>2){
                Thread.yield();
            }
    
            System.out.println(Thread.currentThread().getName()+" "+num);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    以上示例可以说明volatile不保证原子性

    我们已经将num声明为volatile,运行结果之后可以发现,num依然小于20000

    或许会有笔者忘记了为什么num会小于20000,又为什么小于20000会表明程序没有原子性,我们再来回顾一下,没有忘记的读者可以跳过该部分。

    首先,读者需要知道的是,在底层,num++其实并不是一条指令,而是三条指令(感兴趣的小伙伴看图解操作系统),第一步,从内存中读取num的值放到寄存器中,第二步,对寄存器内的值进行+1运算,第三步,将寄存器的值放回到num的地址中。

    该实例开启了20条线程。20条线程都在执行同样的操作,即调用add方法1000次,总过加起来有20个1000。而因为线程之间会相互抢占CPU资源,在没有锁(synchronized、lock等)的情况下,会出现以下情况:

    此时num=50,线程A进入add方法,将num++;即num=51

    但线程A还未写入内存时,便被线程B抢了CPU资源

    因为A没有将51写入内存,B看到的num依旧是50,此时他对num++;然后写入内存

    然后A又抢到CPU资源,因为A之前的操作停留在num++上,也就是说,它的下一步是将num写入内存(机器很傻,它不会察觉自己手上的num已经发生改变,而是遵循着代码的顺序,执行下一条指令,而num++后的下一条指令为“把num写入内存”)它将自己手上的num,也就是51,又写进内存

    因此,A线程与B线程进行了两次num++操作,理应num应该从50变到52,结果只是从50变成51,而在程序中,这种情况可能出现,也可能不会出现,可能在任何时候出现,因此无法预测,每一次运行都是一个全新的结果。

    现在理解为何示例不保证原子性了吧,因为它并没有遵循“线程在执行任务的时候不可被打扰,其操作要么全部成功,要么全部失败”,很显然,A线程在执行任务的时候被中断了,num++成功,但写入内存失败。

    如何保证线程具有原子性呢?

    很简单,使用lock或synchronized锁就好了

    但如果不见lock或synchronized锁,怎样保证原子性呢?

    使用Atomic**

    如:num为integer类型,则使用AtomicInteger

    // 验证 volatile不保证原子性
    public class vDemo02 {
        // 不保证原子性
        //private volatile static int num = 0;
        // 原子性操作
        private static AtomicInteger num = new AtomicInteger();
        public static void add(){
            // num++;
            num.getAndIncrement(); // CAS计算机底层并发原理,效率极高!!
        }
        public static void main(String[] args) {
            for (int i = 1; i <= 20; i++) {
                new Thread(()->{
                    for (int j  = 0; j < 1000; j++) {
                        add();
                    }
                }).start();
            }
    
            while(Thread.activeCount()>2){
                Thread.yield();
            }
    
            System.out.println(Thread.currentThread().getName()+" "+num);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    这些类的底层都是直接与操作系统挂钩,在内存中修改值

    3、禁止指令重排

    什么是指令重排

    你写的程序,计算机并不是按照你写的那样去执行的

    源代码=>编译器优化的重排=>指令并行也可能会重排=>内存系统也会重排=>执行

    处理器在执行指令重排的时候,会考虑数据之间的依赖性,意图保证程序不出错

    但在并发情况下,这种重排可能会产生错误

    例如:

    a,b,c,d四个值默认都是0

    以下是我们开了两个线程,代码顺序为自上往下

    线程A线程B
    x=ay=b
    b=1a=2

    正常结果:x=0 y=0

    但实际上指令重排后可能会产生如下顺序:

    线程A线程B
    b=1a=2
    x=ay=b

    指令重排导致的结果为:x=2 y=1

    volatile可以避免指令重排

    volatile可以生成内存屏障,内存屏障为CPU指令,作用:

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

    2、保证某些变量的内存可见性

  • 相关阅读:
    【RISC-V】Trap和Exception
    2-39 JSP之EL表达式
    C++ set map 的模拟实现
    紧跟热点:教你如何快速掌握ChatGPT
    已解决ValueError: If using all scalar values, you must pass an index
    2.1配置(AutoMapper官方文档翻译)
    Kubernetes:(二)了解k8s组件
    ESP8285 RTOS SDK OTA
    kettle连接达梦资源库-达梦资源库初始化SQL
    【AI】PyTorch入门(六):自动微分torch.autograd
  • 原文地址:https://blog.csdn.net/whale_cat/article/details/133913087