JUC并发编程之volatile详解

目录

1. volatile

1.1 volatile关键字的作用

1.1.1 变量可见性

1.1.2 禁止指令重排序

1.2 volatile可见性案例

1.3 volatile非原子性案例

1.4 volatile 禁止重排序

1.5 volatile 日常使用场景

送书活动


1. 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的修改。

JUC并发编程之volatile详解_第1张图片

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()方法的原子性。

这是什么原因呢?

JUC并发编程之volatile详解_第2张图片

 对于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等一系列详细讲解。让您轻松掌握生图技巧,创造出独特的艺术作品,书写属于自己的艺术时代。

JUC并发编程之volatile详解_第3张图片

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

 关注博主、点赞、收藏、

评论区评论

  即可参与送书活动! 

你可能感兴趣的:(并发编程和高并发实战,java,jvm,开发语言)