文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

JUC并发编程之volatile详解

2023-09-01 11:12

关注

目录

 

1. volatile

1.1 volatile关键字的作用

1.1.1 变量可见性

1.1.2 禁止指令重排序

1.2 volatile可见性案例

1.3 volatile非原子性案例

1.4 volatile 禁止重排序

1.5 volatile 日常使用场景

送书活动


 

在并发编程中,多线程操作共享的变量时,可能会导致线程安全问题,如数据竞争、可见性问题等。为了解决这些问题,Java提供了JUC(java.util.concurrent)工具包,其中包含了很多用于处理并发编程的工具类和接口。在JUC中,volatile是一个关键字,它可以用于修饰变量,用来确保变量的可见性和禁止指令重排序,从而在一定程度上解决线程安全问题。

1.1 volatile关键字的作用

1.1.1 变量可见性

在多线程环境下,如果一个线程修改了共享变量的值,其他线程可能由于线程间的数据不一致性而看不到该变量的最新值。这种问题称为“变量不可见性”或“可见性问题”。

volatile关键字可以确保被修饰的变量对所有线程可见。当一个线程修改了volatile变量的值,其他线程立即能够看到修改后的最新值,而不会使用缓存中的旧值。

1.1.2 禁止指令重排序

在JVM(Java虚拟机)中,为了优化性能,编译器和处理器可能会对指令进行重排序。在单线程环境下,这种重排序不会影响程序的执行结果。然而,在多线程环境下,指令重排序可能会导致线程安全问题。

volatile关键字可以防止指令重排序,确保被修饰的变量按照代码中的顺序执行。

1.2 volatile可见性案例

public class VolatileExample {    private static volatile boolean flag = false;    public static void main(String[] args) {        new Thread(() -> {            while (!flag) {                System.out.println("Waiting for the flag to be true...");            }            System.out.println("Flag is now true. Exiting the thread.");        }).start();        try {            Thread.sleep(1000); // 确保主线程在子线程之前执行        } catch (InterruptedException e) {            e.printStackTrace();        }        System.out.println("Setting the flag to true...");        flag = true;    }}

在上面的示例中,我们创建了一个VolatileExample类,并声明了一个volatile类型的flag变量。在主线程中,我们启动一个新的子线程,该子线程会不断地检查flag变量的值,直到flag变为true时,子线程退出。

在主线程中,我们将flag变量设置为true。由于flag变量被声明为volatile类型,子线程能够及时看到flag的最新值,从而退出循环,输出“Flag is now true. Exiting the thread.”。

这个示例演示了volatile关键字的作用,确保了flag变量的可见性。如果我们没有使用volatile关键字,子线程可能会一直循环下去,因为它看不到主线程对flag的修改。

38b2da1a26484e70b96d28c9a0f348a3.png

 

1.3 volatile非原子性案例

public class Test {    public static void main(String[] args) throws InterruptedException {        VolatileAtomicityExample example = new VolatileAtomicityExample();        for(int i=1;i<=100;i++){            new Thread(()->{                for(int j=1;j<=1000;j++)                    example.increment();            },String.valueOf(i)).start();        }        TimeUnit.SECONDS.sleep(2);        System.out.println(example.getCount());    }}class VolatileAtomicityExample {    volatile int count = 0;    public  void increment() {        count++;    }    public int getCount() {        return count;    }}

我们创建了一个VolatileAtomicityExample类,其中的成员变量count被声明为volatile类型。然后,我们创建了100个线程,每个线程分别执行1000次increment()操作,对count进行自增。最后,我们在主线程中打印count的最终值。以上示例中的输出结果可能会因为运行时的不确定性而有所不同。每次运行时可能得到不同的结果,但通常结果都小于100000。为了解决这个问题,我们需要使用synchronized关键字或其他线程安全机制来确保increment()方法的原子性。

这是什么原因呢?

4afd2ac10749476b910af197364ee512.png

 对于volatile变量具备可见性,JVM只是保证从主内存加载到线程工作内存的值是最新的,也仅是数据加载时是最新的。但是多线程环境下,"数据计算"和"数据赋值"操作可能多次出现,若数据在加载之后,若主内存volatile修饰变量发生修改之后,线程工作内存中的操作将会作废去读主内存最新值,操作出现写丢失问题。即各线程私有内存和主内存公共内存中变量不同步,进而导致数据不一致。由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改主内存共享变量的场景必须使用加锁同步。

1.4 volatile 禁止重排序

内存屏障是一种硬件指令或者编译器指令,用于控制内存操作的顺序,以确保多线程环境下的内存可见性和正确的执行顺序。

内存屏障分为两种类型:

  1. 内存读屏障(Load Barrier):它是一个特殊的硬件或者编译器指令,用于保证在内存读取操作之前,所有的先行写操作都已经完成,并且其结果对当前线程可见。也就是说,读屏障可以防止后续读取指令重排序到读屏障之前的位置。

  2. 内存写屏障(Store Barrier):它是一个特殊的硬件或者编译器指令,用于保证在内存写入操作之前,所有的先行写操作和写屏障之前的写操作都已经完成,并且其结果对其他线程可见。也就是说,写屏障可以防止前面的写入指令重排序到写屏障之后的位置。

volatile关键字通过内存屏障来保证变量的读写操作不会被重排序。具体来说,对于volatile变量的写操作,在写入变量之后会插入写屏障,这样可以防止其他指令重排序到写屏障之前。类似地,对于volatile变量的读操作,在读取变量之前会插入读屏障,这样可以防止其他指令重排序到读屏障之前。

通过这种方式,volatile关键字确保了对变量的读写操作具有一定的有序性,从而保证了多线程环境下的内存可见性和正确的执行顺序。

1.5 volatile 日常使用场景

状态标志:当一个线程修改了某个状态标志,其他线程需要立即看到最新的状态。这时可以使用volatile关键字修饰状态标志,保证其在多线程之间的可见性。例如:

public class Task implements Runnable {    private volatile boolean isRunning = true;    @Override    public void run() {        while (isRunning) {            // 执行任务逻辑        }    }    public void stop() {        isRunning = false;    }}

双重检查锁定(Double-Checked Locking):在多线程环境下,当需要延迟初始化一个对象时,为了避免重复初始化,常常使用双重检查锁定。在这种情况下,需要使用volatile关键字来确保对象在多线程环境中的可见性。例如:

public class Singleton {    private volatile static Singleton instance;    private Singleton() {}    public static Singleton getInstance() {        if (instance == null) {            synchronized (Singleton.class) {                if (instance == null) {                    instance = new Singleton();                }            }        }        return instance;    }}

在上述代码中,我们实现了一个简单的单例模式。在getInstance()方法中,我们使用双重检查锁定来实现延迟初始化,确保只有在instance为空时才创建新的Singleton实例。

然而,在多线程环境下,由于指令重排序的存在,可能会导致以下问题:

  1. 对象引用不为空但尚未初始化:在线程A执行完instance = new Singleton();这一行之前,可能发生指令重排序,导致instance的引用不为空,但是Singleton实例的初始化还未完成。这样,线程B在执行return instance;时就会得到一个尚未初始化的对象,导致错误。

  2. 可见性问题:指令重排序也可能导致线程B无法及时看到线程A的初始化操作。例如,线程A对instance的赋值可能被重排到线程A的后面执行,从而线程B在读取instance时得到一个旧的引用,无法感知线程A的初始化操作。

为了解决这个问题,需要在创建Singleton实例时使用volatile关键字来保证对象的可见性和禁止指令重排序。

 

《硅基物语·我是灵魂画手》

 

当AI遇上绘画,会打开怎样的奇妙世界?

用ChatGPT+Midjourney西出人类的灵魂与梦想

用StableDiffusion+D-ID画出青春绚丽的渴望

激活每个人隐藏的绘画天赋

人人都能成为顶尖绘画大师

 

ChatGPT+Midjourncy+StableDiffusion+D-ID+RunwayGen-l

爆火软件全流程协作

掌据AI绘商技巧

解锁超全绘画关键司

讲解创作底层逻辑

一本书讲透AI绘画全流程

 

内容简介

一本将AI绘画讲透的探秘指南,通过丰富的实践案例操作,通俗易懂地讲述AI绘画的生成步骤生动展现了AI绘画的魔法魅力。从历史到未来,跨越百年时空,从理论到实践,讲述案例操作:从技术到哲学,穿越多个维度,从语言到绘画,落地实战演练。AI绘画的诞生,引发了奇点降临,点亮了AGI(通用人工智能),并涉及 Prompt、风格,技术细节、多模态交互,AIGC等一系列详细讲解。让您轻松掌握生图技巧,创造出独特的艺术作品,书写属于自己的艺术时代。

 

ce601e4efef840d5a5733a2116498ffb.jpeg

 当当网链接:http://product.dangdang.com/29601870.html

 

 关注博主、点赞、收藏、

评论区评论

  即可参与送书活动! 

 

来源地址:https://blog.csdn.net/qq_43649937/article/details/132042338

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯