JUC并发编程

volatile和JMM

1. volatile修饰变量的特点

  1. 高并发场景下经常使用volatile来修饰变量,被volatile修饰的变量有两大特性:可见性和有序性,不满足JMM的原子性
  2. 可见性
    1. 使用volatile修饰的变量始终保证了变量的可见性
  3. 有序性
    1. 在重排之后结果正确的前提下,可以进行重排来提高性能,但有时需要禁止重排
  4. volatile的内存语义
    1. 当线程写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中
    2. 读一个volatile变量时JMM会把该线程对应的本地内存设置为无效重新回到主内存中读取最新的共享变量
    3. 所以volatile的写内存语义是直接刷新到主内存中,读内存语义是直接从主内存中读取
    4. JMM内存模型中每一个线程都有自己的工作内存,然后数据全部存在主内存中,并保证原子性、可见性和有序性
  5. volatile的可见性和有序性
    1. volatile只可以保证可见性和有序性,但无法保证原子性
    2. volatile变量一定存放在主内存中,从而保证可见性
    3. 且写内存时直接写入到主内存,读数据时直接从主内存中读取
    4. volatile通过禁止重排来保证有序性

2. 内存屏障

  1. volatile的两大特性:可见性、有序性
    1. 可见性通过在本地修改后直接将变量直接刷新进主内存并通知来实现,然后从主内存中读取数据,前面的修改对所有的线程是可见的
    2. 有序性通过禁止重排来实现
    3. 不存在数据依赖关系可以进行重新排序,但如果存在数据依赖关系,则必须禁止重排序
    4. 重排后的指令不可以改变原有的串行语义
    5. 只要使用volatile进行修饰,就是可见性+禁止重排
  2. 内存屏障
    1. happens-before先行发生原则就是通过内存屏障来实现
    2. 内存屏障是一类同步屏障指令CPU对内存随机访问的操作中的一个同步点使得同步点之前的所有读写操作都执行后才可以开始执行此点之后的操作
    3. 通过内存屏障能避免代码重排
    4. 内存屏障就是一种JMM指令,重排规则会要求Java编译器在生成指令时插入特定的内存屏障指令,通过内存配置指令,可以实现有序性(禁止重排)
    5. 内存屏障就是一个指令,来定义一个同步点内存屏障之前的所有写操作都要回写到主内存,之后的所有读操作都能够获得之前的写操作的结果,实现了可见性,且实现了指令的禁止重排,实现了有序性
    6. 使用内存屏障可以实现JMM的有序性(定义一个同步点来分隔)和可见性(屏障之前的指令都会写入主内存)
    7. 写屏障之前的数据都会同步到主内存中;读屏障之后的读操作都在读屏障之后执行保证读到的是最新的数据
    8. 在重排序时,必须要严格按照内存屏障的要求,不允许把内存屏障之后的指令重排到内存屏障之前
    9. 故对于volatile变量,因为存在内存屏障保证的有序性,其所有的读操作都必须在写操作之后才可以进行
    10. 指令重排序时要按照内存屏障的要求,不允许把内存屏障之后的指令重排到内存屏障之前
  3. 内存屏障分类
    1. happens-before先行发生原则是一个接口规范,通过内存屏障来实现
    2. 内存屏障是一个同步点只有之前的指令执行结束之后才可以执行之后的指令
    3. 读写屏障
      1. 读屏障:在读指令之前插入读屏障让工作内存中的数据失效重新到主内存中获取最新数据
      2. 写屏障:在写指令之后插入写屏障强制把缓冲区中的数据写回主内存中
      3. 全屏障:之前的写指令全部写入主内存,之后的读指令全部从主内存中读取
    4. 四种屏障
      1. 内存屏障粗分是读写屏障两种,细分是四种屏障![[Pasted image 20241216123800.png]]

      2. 内屏屏障是插入在两个指令之间的

      3. 写屏障保证之前的写操作已经刷新到主内存中读屏障保证之后的读操作从主内存中读取

    5. 指令重排序时不可以把屏障之后的指令重排到屏障之前
  4. 1

3. volatile特性:有序性、可见性

  1. 通过将写操作之后工作内存中的数据直接刷新进入主内存中以及从主内存中读取数据来保证可见性
  2. 通过在不同的操作指令之间插入内存配置来保证有序性;内存屏障之后的指令必须在内存屏障之前指令执行结束后才可以执行不可以将之后的指令重排到之前
  3. 通过四种内存屏障来禁止重排,设置了内存屏障后就可以保证屏障之后的指令一定不会重排到屏障之前

4. happens-before之volatile变量规则

  1. 当第一个操作是volatile读时不可以重排序,保证读之后的操作无法重排序到读之前
  2. 当第二个操作是volatile写操作时,不可以重排序,保证写之前的操作无法重排序到写之后
  3. 第一个操作是volatile写,第二个操作是volatile读时不可以重排
  4. 在每个volatile读操作后面插入一个读屏障(LoadLoad和LoadStore),故第一个操作为volatile读操作时不可以重排;在每个volatile写操作之前插入一个写屏障(StoreStore),后面插入一个(StoreLoad屏障)
  5. ![[Pasted image 20241216125046.png]]

5. volatile

  1. 保证可见性
    1. 保证不同线程对某个变量完成操作后及时可见,即该共享变量一旦改变所有线程立即可见
    2. 可见性是指某个线程对某个变量修改后,所有的线程都可见
    3. 不加volatile,是没有可见性的,此时的每个线程中的数据都存放在自己的工作内存区,修改后不是可见的,不会立即修改进入主内存
    4. 加了volatile才有可见性,必须用volatile修饰的变量才有可见性,此时进行的写操作会直接修改进入主内存中,且此时的读操作也是从主内存中读取
    5. 通过内存屏障来实现,读屏障前后的所有读操作都会从主从中读取,写屏障前后的所有写操作都会写入主内存
    6. 被volatile修饰的变量每次读取时都会去主内存中读取,每次写入时都会直接写入主内存,保证了可见性,此时某个线程的修改对所有的线程都可见
  2. volatile变量的读写过程
    1. 从主内存读取到工作内存,然后使用进行修改,然后先写入工作内存再写入主内存,写入时必须加锁,写完之后解锁,加锁后会清空工作内存的变量,下次使用会重新读取 ![[Pasted image 20241216131837.png]]![[Pasted image 20241216132352.png]]

    2. 每个单独的操作都是原子操作,能够保证原子性,但对于多个指令的组合,没有保证原子性,故要使用加锁和解锁指令来保证原子性,加锁是对volatile变量进行加锁,解锁后会清除所有的工作内层,下次必须重新读取

  3. 没有原子性
    1. volatile关键字修饰的变量的操作没有原子性,此时需要手动加上synchronized来实现原子性
    2. 多线程操作共享变量时,必须要使用volatile关键字进行修饰,否则此时的变量不是可见的
    3. volatile关键字修饰的变量的操作是不具有原子性的,如要实现原子性,必须要加锁,使用synchronized来修饰
    4. volatile修饰的变量具有原子性,对于不同的线程都是可见的,都是直接修改进入主内存且从主内存中读取,但不具有原子性,如要实现原子性必须要使用synchronized来加锁,因为具有可见性,所以所有的线程都可以直接操作,但不具有原子性
    5. 使用synchronized对共享变量进行修改时,此时修改后会立即同步到主内存中,且解锁后读取时从主内存中读取
    6. synchronized自带可见性,此时对变量的操作一定会同步到主内存中
    7. volatile修饰的变量只有可见性和有序性,但没有原子性,此时可能存在写丢失操作,即多个线程同时写操作,但最终结果被覆盖,并发写问题
    8. synchronized关键字不仅有原子性,而且还有可见性,每次加锁解锁后都会更新主内存中的数据;volatile变量只具有可见性,但不具有原子性,可能存在写丢失
    9. 原子性指的是一个操作是不可中断的,一个操作一旦开始就不会被其他线程影响,原子性的操作要么全部执行成功,要么全部执行失败,synchronized加锁具有原子性,保证了一次只有一个线程执行,而volatile修饰的变量不具有原子性,其只可以保证可见性,但不具有原子性,多个线程会存在并发问题
  4. 指令禁止重排
    1. 在对volatile变量进行操作时,会自动插入若干读写屏障指令来保证有序性,使用读写屏障指令禁止重排来确保有序性
    2. 只要存在依赖关系就必须禁止重排,如果不存在依赖关系说明此时顺序无影响,此时就可以重排
    3. 不存在数据依赖的指令是可以重排序的,但存在数据依赖的指令必须保证逻辑顺序
    4. 单线程的数据依赖可以很容易区分,但是多线程的情况下,此时的数据依赖很难区分多线程下的数据依赖关系很难区分
    5. 在多线程的情况下,指令重排序可能会出现错误,但是volatile变量保证了有序性,通过内存屏障来实现
    6. volatile变量读操作会在之后插入一个LoadStore和LoadLoad来保证不可重排;在写操作之前插入一个StoreStore指令来确保当前写操作之前的所有写操作都写入了主内存,在之后插入一个StoreLoad操作来保证之后的读在当前写入主内存之后
    7. volatile变量的禁止重排序是禁止同一个线程中的指令重排序,此时禁止的是同一个线程中指令的重排序
    8. volatile变量的禁止重排禁止的是对同一个线程中的指令的重排
    9. 同一个线程中的volatile读操作之后的操作禁止重排序,volatile写操作之前之后的都禁止重排序
  5. 如果正确使用volatile
    1. volatile变量是在多线程下的共享变量,保证多个线程间的可见性以及有序性,但不保证原子性(i++时存在线程安全),适用于修饰表示状态的变量,此时修改一定是原子性的,但不适合用存在依赖关系的数据
    2. 适合多线程下单一赋值的共享变量,但对于复合运算的变量不适合,因为不保证原子性
    3. 适用于状态标志,判断业务是否结束
    4. 适用于写锁策略,当读远大于写时,此时就可以使用volatile修饰,此时读取操作使用volatile,写操作使用synchronized,如果不使用volatile,则读操作也必须使用synchronized加锁,此时就不支持并发读,故可以使用volatile来保证读操作的可见性
    5. 单例设计模式下双端锁的发布双检加锁,先判断单例是否存在,不存在时加锁,加锁内再次判断是否存在,从而确保只进行一次创建单例双检加锁来确保只创建一次单例,此时的单例就可以使用volatile修饰,从而禁止重排序,确保创建单例时不被禁止重排序
    6. volatile变量保证了可见性和有序性,但无法保证原子性,故适用于多线程(可见性)下的单一赋值的变量(不需要原子性),但不适用于复合操作赋值的变量
  6. 小总结
    1. volatile通过读写屏障来实现读写操作的可见性和有序性,读屏障之后的读操作会从主内存中读出,写屏障之前的写操作会全部写入主内存中
    2. volatile常用来保存某个状态的boolean值或者int值,不适合参与依赖当前值的运算
    3. 因为volatile只具有可见性,故适合用来保存某个状态的boolean值但因为不存在原子性,故不适合参与依赖当前值的运算
    4. i++运算底层是分三步进行的,此时如果使用volatile修饰则无法保证原子性,因为在执行过程中存在线程安全问题

6. JMM

  1. JMM内存模型是针对多线程下的内存访问规范,要实现原子性、可见性和有序性
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值