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);
    }
}

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);
    }
}

以上示例可以说明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);
    }
}

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

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、保证某些变量的内存可见性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

苏三有春

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值