volatile 详解

Java 内存模型(Java Memory Model)

  • JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着 CPU 寄存器、缓存、硬件内存、CPU 指令优化等
  • JMM主要体现在以下几个方面
    原子性(多写多读):保证指令不会受到线程上下文切换的影响
    可见性(一写多读):保证指令不会受到CPU缓存的影响
    有序性:保证指令不会受到CPU指令并行优化的影响

原子性问题—得不到预期值

public class AtomQuestion {
    static int counter = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++)
            // 临界区
            {
                counter++;
            }
        }, "t1");
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++)
            // 临界区
            {
                counter--;
            }
        }, "t2");
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        MyTool.printTimeAndThread("计算后的初始化值为:" + counter);
    }
}

问题分析:

  • 由于counter–与counter++操作在不同的线程中执行且操作的是同一个变量,所以这两个操作为临界区,那么在执行就会发生竞态条件
  • 在发生竞态条件时,有没有对其加锁,所以会导致存、取数据时发生被其他线程修改,自己又无法感知的情况
  • 这样执行完代码,肯定拿不到预期值0

可见性问题—退不出的循环

public class VisibilityQuestion {
    static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            while (flag) {
                System.out.println();
            }
        }, "t1");
        t1.start();
        TimeUnit.SECONDS.sleep(1);
        // 线程t不会如预想的停下来
        flag = false;
    }
}

问题分析:

  • .初始状态, t1线程刚开始从主内存读取了 run 的值到工作内存
  • 由于 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率
  • 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

有序性问题—不可预见的结果

public class OrderlyQuestion {
    int num = 0;
    boolean ready = false;
    // 线程1 执行此方法
    public void actor1(Object r) {
        if(ready) {
            r = num + num;
        } else {
            r = 1;
        }
    }
    // 线程2 执行此方法
    public void actor2(Object r) {
        num = 2;
        ready = true;
    }
}

结果集:

  • 线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
  • 线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
  • 线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)
  • 【不是预期的结果】线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2(这种现象叫做指令重排,是JIT编译器在运行时的一些优化)
指令重排
  • JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,如下面例子
// 可以重排的例子
int a = 10; // 指令1
int b = 20; // 指令2
System.out.println( a + b );
// 不能重排的例子
int a = 10; // 指令1
int b = a - 5; // 指令2
  • 指令重排序优化
  1. 现代处理器会设计为一个时钟周期完成一条执行时间最长的 CPU 指令。为什么这么做呢?可以想到指令还可以再划分成一个个更小的阶段,例如,每条指令都可以分为: 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 这 5 个阶段
  2. 在不改变程序结果的前提下,这些指令的各个阶段可以通过重排序和组合来实现指令级并行,这一技术在 80’s 中叶到 90’s 中叶占据了计算架构的重要地位
  • 支持流水线的处理器
  1. 现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一条执行时间最长的复杂指令),IPC = 1,本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了指令地吞吐率。

解决有序性、可见性问题我们可以使用关键字 【volatile】,原子性的解决方案我们可以使用关键字 volatile+ 原子类的CAS操作(乐观锁机制) 或者使用锁(悲观锁机制)

内存屏障(Memory Fence)

  • 保证可见性
    写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
    读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

  • 保证有序性
    写屏障(sfence)会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
    读屏障(lfence)会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

  • 不能保证原子性

volatile 原理

volatile 的底层实现原理是内存屏障(Memory Fence)

  • 对 volatile 变量的写指令后会加入写屏障
  • 对 volatile 变量的读指令前会加入读屏障

如何保证可见性

写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

public void actor2(I_Result r) {
 num = 2;
 ready = true; // ready 是 volatile 赋值带写屏障
 // 写屏障
}

读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

public void actor1(I_Result r) {
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) {
 r.r1 = num + num;
 } else {
 r.r1 = 1;
 }
}

如何保证有序性

写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

public void actor2(I_Result r) {
 num = 2;
 ready = true; // ready 是 volatile 赋值带写屏障
 // 写屏障
}

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

public void actor1(I_Result r) {
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) {
 r.r1 = num + num;
 } else {
 r.r1 = 1;
 }
}

如何保证原子性

可以使用原子类中的cas + volatile关键字解决多线程并发问题。

转账问题
  • 接口
package com.yanxb.threadnolock;

import java.util.ArrayList;
import java.util.List;

interface Account {
    /**
     * 获取余额
     */
    Integer getBalance();

    void withdraw(Integer amount);

    /**
     * 该方法会启动1000个线程,每个线程扣除10元,若初始值为10000则余额刚好为0
     * */
    static void demo(Account account) {
        List<Thread> threads = new ArrayList<>();
        long start = System.nanoTime();
        for (int i = 0; i < 1000; i++) {
            threads.add(new Thread(() -> {
                account.withdraw(10);
            }));
        }
        threads.forEach(Thread::start);
        threads.forEach(thread -> {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println("余额为:" + account.getBalance() + "  花费的时间为:" + (end - start)/1000000 + "ms");
    }
}
  • 不安全实现类
package com.yanxb.threadnolock;

public class ThreadUnsafeImpl implements Account {
    private Integer balance;
    public ThreadUnsafeImpl(Integer balance) {
        this.balance = balance;
    }
    @Override
    public Integer getBalance() {
        return balance;
    }
    @Override
    public void withdraw(Integer amount) {
        balance -= amount;
    }
}
  • 解决方式一:加锁使用Synchronized关键字
package com.yanxb.threadnolock;

public class ThreadSafeImplBySynchronized implements Account{
    private Integer balance;
    public ThreadSafeImplBySynchronized(Integer balance) {
        this.balance = balance;
    }
    @Override
    public synchronized Integer getBalance() {
        return balance;
    }
    @Override
    public synchronized void withdraw(Integer amount) {
        balance -= amount;
    }
}

解决方式二:使用原子类的cas操作 + volatile

package com.yanxb.threadnolock;

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadSafeByNoLock implements Account{

    private volatile AtomicInteger balance;

    public ThreadSafeByNoLock(Integer balance) {
        this.balance = new AtomicInteger(balance);
    }

    @Override
    public Integer getBalance() {
        return balance.get();
    }

    @Override
    public void withdraw(Integer amount) {
        // 需要不断尝试,直到成功为止
        while (true){
            // 获取旧值
            int prev = balance.get();
            // 设置要转换的值
            int next = prev - amount;
            /*compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值不一致了,next 作废,返回 false 表示失败
              比如,别的线程已经做了减法,当前值已经被减成了990那么本线程的这次990就作废了,进入while下次循环重试一致,以next设置为新值,返回true表示成功
             */
            if (balance.compareAndSet(prev,next)) {
                break;
            }
        }
    }
}
  • 测试类
    public static void main(String[] args) {
        // Account.demo(new ThreadUnsafeImpl(10000));
        // Account.demo(new ThreadSafeImplBySynchronized(10000));
        Account.demo(new ThreadSafeByNoLock(10000));
    }
CAS

概述

  • 上述解决转账问题方式二中的compareAndSet操作,它的简称就是 CAS (也有 Compare And Swap 的说法),它是原子操作
  • cas是原子操作的原理是底层使用了指令 lock cmpxchg,在X86架构下,无论是单核还是多核都可以保证cas操作都是原理的
  • 单核情况下就不解释里,在多核情况下,无论哪个核在执行带有lock指令时,CPU都会让总线锁住,当这个核将这条带有lock指令结束后,在开启总线。这个过程是不会被任务调度器打断的,从而保证了cas操作的原子性。

特点

  • 原子类cas + volatile可以实现无锁并发,使用与多核、线程数少的场景
  • cas的实现是基于乐观锁思想:其他线程修改了值,重试,没有则操作成功。而synchronized是基于悲观锁的思想:当我修改时,其他线程都不可以对共享变量修改
  • cas体现的是无锁并发、无阻塞并发。因为不会使用synchronized,所以不会导致线程进入阻塞,也不会导致上下文切换,从而提高效率。但如果竞争激烈时,重试次数必然会频繁发生,从而导致效率下降。所以在使用时需要权衡利弊

与volatile关系

  • 原子类在cas操作时,必须保证共享变量的可见性,所以就需要用volatile关键字修饰。这也是原子类中存储值的变量使用volatile关键字修饰的原因。
  • 线程操作 volatile 变量都是直接操作主存,即一个线程对 volatile 变量的修改,对另一个线程可见
  • 但volatile 仅仅保证了共享变量的可见性,让其它线程能够看到最新值,并不能解决指令交错问题

原子类详解:https://blog.csdn.net/silence_yb/article/details/124163132

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

似寒若暖

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

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

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

打赏作者

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

抵扣说明:

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

余额充值