volatile(一)作用与原理

目录

Java 内存模型

可见性 

退不出的循环 

原因分析 

解决方案 

synchronized

System.out.println()

volatile 

volatile 保证原子性? 

volatile 保证可见性原理 

验证写屏障

验证读屏障

有序性 

想不到的结果 

原因分析 

指令级并行原理

名词

工厂加工零件 

支持流水线的处理器 

指令重排序优化 

保证有序性 


Java 内存模型

JMM(Java Memory Model),定义了主存(共享)、工作内存(私有)抽象概念,底层对应着 CPU 寄存器缓存硬件内存CPU 指令优化

② JMM 体现在以下几个放面(并发安全也如此):

     2.1 原子性:保证指令不受到线程上下文切换影响(指令交错)

     2.2 可见性:保证指令不受 CPU 缓存影响

     2.3 有序性:保证指令不受 CPU 指令并行优化影响(指令重排)

可见性 

退不出的循环 

    static void cannotFinish() throws InterruptedException {
        new Thread(() -> {
            while(flag){
            }
            log.debug("收到,结束!");
        }, "t").start();
        
        log.debug("t 启动!");
        Thread.sleep(1000);
        log.debug("t 该结束了");
        flag = false;
    }

线程 t 循环并没有结束

原因分析 

① 初始线程 t 从主存读取 flag工作内存,为 true 则继续循环

② 由于线程 t 反复从主存读取 flagJIT 编译器将 flag 的值缓存至线程 t 工作内存中的高速缓存中,减少对主存的访问,提高效率

③ 1s 后,主线程将 flag 值改为 false,并同步至主存;而线程 t从工作内存中的高速缓存中读取 flag 值,读到的永远是旧值

解决方案 

synchronized

synchronized 可以保证 flag 可见性,它会使线程 t 只从主存中取 flag 的值(同步代码块中

        new Thread(() -> {
            while(true){
                synchronized (VolatileTest.class){
                    if(!flag)break;
                }
            }
            log.debug("收到,结束!");
        }, "t").start();

System.out.println()

private static boolean flag = true;

while(flag){
   System.out.println(flag);
}

底层实现用到了 synchronized,会保证 flag 从主存中读取

volatile 

① volatile 也可以保证 flag 的可见性,线程 t 每次都从主存中取 flag 值(无论在哪里)

② 可见性是对于共享资源的,当一个线程对该资源进行修改其他线程能马上看到

单线程或者局部变量无需担心可见性问题

private static volatile boolean flag = true; // volatile 修饰 flag

volatile 保证原子性? 

volatile 是不保证原子性的 

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for(int i = 0; i < 5000; i++){
                count++;
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for(int i = 0; i < 5000; i++){
                count--;
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        log.debug("count: {}", count);
    }

① volatile 保证了 count 的可见性;即线程 t1、t2 每次都是从主存取值,保证每次取的都是最新值

② 但是无法避免上下文切换(指令交错)带来的影响

volatile 保证可见性原理 

① 被 volatile 修饰的变量,变量⭐进行读操作⭐时,在变量前面有一个“读屏障”(lfence);“读屏障”保证它后面对共享变量的读取操作,都是从主存取 

② 被 volatile 修饰的变量,变量⭐进行写操作⭐时,在变量后面有一个“写屏障”(sfence);“写屏障”保证它前面对共享变量的更新操作,都同步到主存 

验证写屏障

    private static int mark1 = 0;
    private static int mark2 = 0;
    private static volatile int mark3 = 0;

    static void fence() throws InterruptedException {
        new Thread(() -> {
            while(mark3 == 0){
            }
            log.debug("mark1 = {}, mark2 = {}, mark3 = {}; 结束!", mark1, mark2, mark3);
        }, "t").start();

        log.debug("开始!");
        Thread.sleep(1000);
        mark1 = 1;
        mark2 = 2;
        mark3 = 3; // 验证写屏障
// --------------------------- 写屏障 -----------------------------------------------------
    }

可以看到, mark3 之前的 mark2、mark1,更新之后的值都同步到了主存

验证读屏障

 对 mark3 的判断,放在 mark1、mark2 之后无法结束 

while(mark1 == 0 || mark2 == 0 || mark3 == 0){ // mark3 放在最后
    mark1
    mark2
//------------------------------ 读屏障 ---------------------------------------------------
    mark3
}

while(mark1 == 0 || mark3 == 0 || mark2 == 0){ // mark3 放在中间
    mark1
//------------------------------ 读屏障 ---------------------------------------------------
    mark3
    mark2
}

 对 mark3 的判断,放在 mark1、mark2 之前可以结束 

while(mark3 == 0 || mark1 == 0 || mark2 == 0){
//--------------------------------- 读屏障 ------------------------------------------------
    mark3
    mark1
    mark2
}

有序性 

想不到的结果 

    static void get(){ // 线程 t1
        if(flag){
            log.debug("count : {}", count);
        }else{
            count = 1;
            log.debug("count : {}", count);
        }
    }

    static void put(){ // 线程 t2
        count = 2; // 操作 1
        flag = true; // 操作 2
    }

线程 t1 执行方法 get,线程 t2 执行方法 put;求最终 count 打印的值?

按平常的分析来:

    ① 线程 t1 先执行,则此时 flag = false;打印 “count :1

    ② 线程 t2 先执行完了操作 1,则此时 flag = false,count = 2;打印“count :1” 

    ③ 线程 t3 先执行完了操作 1 和 2,则此时 flag = true,count = 2;打印“count :2

然而还有一种打印结果:“count :0”

原因分析 

① 因为当 flag = false 时, count = 1

② 所以得 flag = true,并且此时 count 还处于初始化状态

只有一种可能:线程 t2 中,操作 2 比 操作 1 先执行

指令级并行原理

名词

时钟周期时间(Clock Cycle Time) CPU 能识别的最小时间单位,等于主频的到数(4G 主频,时钟周期时间:0.25 ns);生活中的钟表的时钟周期时间:1s

CPI 指令平均时钟周期数(Cycles Per Instruction)

IPC 单位时钟周期运行指令数(Instruction Per Clock Cycle),CPI 的倒数

④ 程序 CPU 执行时间 = 指令数 * CPI * Clock Cycle Time

工厂加工零件 

① 一台计器对零件进行五步加工,每个步骤需要 1 分钟,则加工完 1 个零件,需要 5 分钟 

② 并不是下图这种流程;零件 2 并不需要等待 零件 1 加工完才能加工

③ 从图 ① 也可以得出下图;不同零件的不同加工步骤,是可以并行执行的(零件1 进行第 5 步加工时,零件 2 在进行第 4 步加工,零件 3 在进行第 3 步加工 ......);最好情况是,一分钟做五件事现实生活中的流水线作坊,加工过程差不多是这样

支持流水线的处理器 

① 现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令 

② 每条指令可以划分为 5 个步骤:

    1. instruction fetch(IF) 取指令

    2. instruction decode(ID) 指令译码

    3. execute(EX) 指令执行

    4. memory access(MEM内存访问

    5. register write back(WB) 数据写回 

③ 现代 CPU 支持多级指令流水线,能同时处理如上 5 个步骤的,称为五级指令流水线单条指令执行时间不会改变,但是变相提高了指令吞吐量

指令重排序优化 

① 为了达到不同指令不同阶段并行执行,可能会对指令进行重排序(第 2 条指令,可能被重排到第 1 条指令之前)

不改变程序结果的前提下,可以通过组合重排序来实现指令各阶段指令级并行

分工分阶段是提升效率的关键

int a = 10; // 1
int b = 20; // 2
// 12,21 都没影响,所以可以重排序

int a = 10; // 1
int b = a + 20; // 2
// 12 可以,21 会报错;
// b 需要使用 a,假如 b = a + 20 先执行,a 都还没定义,会报错;所以不能重排序

保证有序性 

① 可以使用 volatile 保证有序性(不发生指令重排

② 被 volatile 修饰的变量,在变量进行读操作时,会在变量前面加一个“读屏障” ;保证它后面的指令不会重排到它前面;但是不保证后面一堆指令不重排

volatile int a = 0;
int b = 0;
int c = 0;
int d = 0;

void sfence(){
    log.debug("b: {}", b); // 不受读屏障影响
//----------------------------------- 读屏障 ----------------------------------------------
    log.debug("a: {}", a);
    log.debug("c: {}", c);
    log.debug("d: {}", d);
}

③ 被 volatile 修饰的变量,在变量进行读操作时,会在变量后面加一个“写屏障”; 保证它前面的指令不会重排到它后面;但是不保证前面一堆指令不重排

volatile int a = 0;
int b = 0;
int c = 0;
int d = 0;

void sfence(){
    b = 1;
    a = 1;
//----------------------------------- 写屏障 ----------------------------------------------
    c = 1; // 不受写屏障影响
    d = 1; // 不受写屏障影响
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值