《JAVA核心技术卷·I》——第十二章11——同步7——volatile——《并发》

🌈hello,你好鸭,我是Ethan,西安电子科技大学大三在读,很高兴你能来阅读。

✔️目前博客主要更新Java系列、项目案例、计算机必学四件套等。
🏃人生之义,在于追求,不在成败,勤通大道。加油呀!

🔥个人主页:Ethan Yankang
🔥推荐:史上最强八股文||一分钟看完我的几百篇博客

🔥温馨提示:划到文末发现专栏彩蛋   点击这里直接传送

🔥本篇概览:详细讲解了volatile字段的引出与作用以及在面对多线程安全问题的用途。🌈⭕🔥


【计算机领域一切迷惑的源头都是基本概念的模糊,算法除外】


🌈序言:

JAVA基础必序扎实的一批,此关不过,啥都没有。今日得《JAVA核心技术·卷I》之良品辅助,应按本书学之习之,时时复习,长此以往必能穿魂入脉,习得大功。

感谢Cay S. Horstmann给世界留下如此优美的作品。

对于一个强烈想完全掌握JAVA的技术宅来说,JAVA的XXX万万不能放过,这些基础的概念例程都值得细细体味的,千万别觉得都是文字,浪费时间,记住——别违背科学发展的客观规律。别一味地赶进度以满足自己学的都么快的虚荣心,自欺欺人,要老老实实的走好每一步。

上一篇文章详细讲解了监视器在同步之中的假设以及synchronized中监视器的体现,建议先将这部分知识掌握之后再来学习本篇内容,点击查看。


🔥《JAVA核心技术卷·I》——第十二章10——同步6——监视器——《并发》-CSDN博客

🔥 所有JAVA基础一键查阅(含习题集)-CSDN博客


🌈引出

但是同学们想过没有,如果因为一两个实例字段的保护而使用同步,是不是会造成巨大的开销?今天这篇文章我们就来详细讲解进一步降低因同步带来的开销但又能达到相同的效果的volatile关键字


🌈同步机制带来的开销

  1. 上下文切换开销:当一个线程获取锁而其他线程被阻塞时,操作系统可能需要进行上下文切换,将当前运行的线程挂起并切换到另一个可运行的线程。上下文切换会消耗一定的 CPU 时间和系统资源
  2. 线程阻塞和唤醒的开销:被阻塞的线程需要在等待锁释放时进入阻塞状态,当锁可用时又需要被唤醒。这涉及到操作系统的线程调度和管理操作,会产生一定的开销
  3. 性能损耗:即使没有发生线程竞争,同步机制本身也会带来一些性能损耗,因为它需要额外的指令和操作来管理锁的获取和释放。

WHY

🌈JMM

🌈现代编译器的处理器的问题

有时,如果只是为了读写一两个实例字段而使用同步,所带来的开销好像有些划不来。但是,使用现代的处理器与编译器,出错的可能性依然很大。原因如下:


  1. 有多处理器的计算机能够暂时在寄存器或本地内存缓存中保存内存值。其结果是,运行在不同处理器上的线程可能看到同一个内存位置有不同的值。

涉及到了JMM模型,JAVA内存模型。

一个内存值被一个线程修改后,这个修改可能不会立即同步到其他处理器的缓存或寄存器中。

不同的处理器可能会暂时缓存同一个内存位置的旧值导致运行在这些处理器上的线程看到的该内存位置的值不一致。

例如,假设线程 A 在处理器 1 上运行,将某个变量 X 的值从 0 修改为 1。但这个修改可能还没来得及传播到处理器 2 的缓存中。此时,如果线程 B 在处理器 2 上读取变量 X,它可能仍然得到旧值 0,而不是最新的 1。


  1. 编译器可以改变指令执行的顺序以使吞吐量最大化。编译器不会选择可能改变代码语义的顺序,但是编译器有一个假定,认为内存值只在代码中有显式的修改指令时才会改变。然而,虽然这个线程没有,内存值有可能被另一个线程改变!

比如,有一段代码,先读取变量 Y 的值,然后进行一些计算,最后再根据条件修改变量 Y 的值。再该线程中编译器不会选择明显修改指令的代码顺序编译,但是编译器可能会为了优化,先执行后面的计算,然后再读取变量 Y 的值。但如果在这期间另一个线程修改了变量 Y 的值,就可能导致本线程的计算结果出现错误。



这两种情况都说明了在多线程编程中,由于硬件和编译器的优化,可能会出现内存可见性和指令执行顺序不一致的问题,从而导致程序的错误行为。



HOW

🌈解决方案:

🌈volatile关键字的使用场景

如果你使用锁来保护可能被多个线程访问的代码,那么不存在这种问题。编译器被要求在必要的时候刷新本地缓存来支持锁,而且不能不相应地重新排列指令顺序。


例如,假设一个对象有一个boolean字段标记done,它的值由一个线程设置,而由另一个线程查询,如同我们讨论过的那样,你可以使用锁:

或许使用内部对象锁不是个好主意。如果另一个线程已经对该对象加锁,isDone和setDone方法可能会阻塞。如果这是个问题,可以只为这个变量使用一个单独的锁。但是,这会很麻烦。带来的性能损失也很大,杀敌1000自损800.


在这种情况下,将字段声明为volatile就很合适

volatile关键字为实例字段的同步访问提供了一种免锁机制

如果声明一个字段为volatile,那么编译器和虚拟机就知道该字段可能被另一个线程并发更新

此时,编译器就会插入适当的代码,以确保如果一个线程对done变量做了修改,这个修改对读取这个变量的所有其他线程都可见



🌈volatile关键字的两大作用:

  • 1.🌈保证可见性

  1. 当一个线程修改了一个被 volatile 修饰的变量时,它会立即将修改后的值刷新到主内存
  2. 其他线程读取该变量时,会强制从主内存中重新获取最新的值,而不是使用本地缓存中的值。

例如,有两个线程 Thread1 和 Thread2 ,共享一个 volatile 变量 count 。Thread1 对 count 进行了增加操作并修改了其值。由于 count 是 volatile 的,这个修改会立即反映到主内存中。当 Thread2 读取 count 时,会直接从主内存获取最新的值,而不是可能的旧缓存值。

🌈代码验证:

import java.time.LocalDateTime;

public class Volatile {

    // 使用 volatile 修饰变量 sharedValue
    private  int sharedValue = 0;

    public static void main(String[] args) {
        Volatile example = new Volatile();

        // 创建一个线程用于修改 sharedValue
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                example.sharedValue++;
                System.err.println("Thread 1 修改: " + example.sharedValue+"\t[time]"+ LocalDateTime.now());
            }
        }).start();

        // 创建另一个线程用于读取 sharedValue
        new Thread(() -> {
            int j=1;
            for (int i = 0; i < 100; i++){
                int value = example.sharedValue;
                System.out.println("第"+(j++)+"次Thread 2 读取: " + value+"\t[time]"+ LocalDateTime.now());
            }
        }).start();
    }
}



  • 2.🌈禁止指令重排序

编译器和处理器在优化代码执行时,可能会对指令的执行顺序进行重排序。但对于被 volatile 修饰的变量,其读写操作不能被重排序到其他内存操作之前或之后。

使用JCstress框架来进行测试:

package com.it.heima;

import org.openjdk.jcstress.annotations.*;
import org.openjdk.jcstress.infra.results.II_Result;

@JCStressTest
@Outcome(id = {"0,0","1,1","0,1"},expect = Expect.ACCEPTABLE,desc = "ACCEPTABLE")
@Outcome(id = {"1,0"},expect = Expect.ACCEPTABLE_INTERESTING,desc = "INTERESTING")
@State

public class ReorderTest {
    int x;
    int y;

    @Actor
    public void actor1(){
        x=1;
        y=1;
    }

    @Actor
    public void actor2(II_Result r){
        r.r1=y;
        r.r2=x;
    }
}

结果发现,(0,0)(0,1)(1,1)是可行的,但是(1,0)就进行了指令重排序。因为在执行actor1时,先执行了y=1,而非x=1.这会导致一些意想不到的错误。

🌈解决方案

在变量上添加volatile,禁止指令重排序,则可以解决问题

屏障添加的示意图

  • 写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下

  • 读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上

其他补充

我们上面的解决方案是把volatile加在了int y这个变量上,我们能不能把它加在int x这个变量上呢?

下面代码使用volatile修饰了x变量

屏障添加的示意图

这样显然是不行的,主要是因为下面两个原则:

  • 写操作加的屏障是阻止上方其它写操作越过屏障排到volatile变量写之下

  • 读操作加的屏障是阻止下方其它读操作越过屏障排到volatile变量读之上


🌈volatile使用技巧

所以,现在我们就可以总结一个volatile使用的小妙招:

  • 写变量让volatile修饰的变量的在代码最后位置

  • 读变量让volatile修饰的变量的在代码最开始位置



💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖💖

热门专栏推荐

🌈🌈计算机科学入门系列                     关注走一波💕💕

🌈🌈CSAPP深入理解计算机原理        关注走一波💕💕

🌈🌈微服务项目之黑马头条                 关注走一波💕💕

🌈🌈redis深度项目之黑马点评            关注走一波💕💕

🌈🌈JAVA面试八股文系列专栏           关注走一波💕💕

🌈🌈JAVA基础试题集精讲                  关注走一波💕💕   

🌈🌈代码随想录精讲200题                  关注走一波💕💕


总栏

🌈🌈JAVA基础要夯牢                         关注走一波💕💕  

🌈🌈​​​​​​JAVA后端技术栈                          关注走一波💕💕  

🌈🌈JAVA面试八股文​​​​​​                          关注走一波💕💕  

🌈🌈JAVA项目(含源码深度剖析)    关注走一波💕💕  

🌈🌈计算机四件套                               关注走一波💕💕  

🌈🌈数据结构与算法                           ​关注走一波💕💕  

🌈🌈必知必会工具集                           关注走一波💕💕

🌈🌈书籍网课笔记汇总                       关注走一波💕💕         



📣非常感谢你阅读到这里,如果这篇文章对你有帮助,希望能留下你的点赞👍 关注❤收藏✅ 评论💬,大佬三连必回哦!thanks!!!
📚愿大家都能学有所得,功不唐捐!

  • 18
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值