volatile关键字原理的使用介绍和底层原理解析和使用实例

这篇具有很好参考价值的文章主要介绍了volatile关键字原理的使用介绍和底层原理解析和使用实例。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

volatile关键字原理的使用介绍和底层原理解析和使用实例

1. volatile 关键字的作用

volatile 关键字的主要作用是保证可见性和有序性,禁止编译器优化。

  • 保证可见性:当一个变量被声明为 volatile 之后,每次读取这个变量的值都会从主内存中读取,而不是从缓存中读取,这就保证了不同线程对这个变量操作的可见性。
  • 有序性:volatile 关键字保证了不同线程对一个 volatile 变量的读写操作的有序性。
  • 禁止编译器优化:编译器会对代码进行各种优化来提高性能,但是这些优化也可能让同步代码失效。volatile 关键字告诉编译器不要对这段代码做优化,从而避免一些不正确的优化。

2. volatile 的底层原理

volatile 关键字底层原理依赖于内存屏障和缓存一致性协议。

  • 内存屏障:内存屏障会强制让读和写操作都访问主内存,从而实现可见性。volatile 写操作后会加入写屏障,volatile 读操作前会加入读屏障。
  • 缓存一致性协议:每个处理器都有自己的高速缓存,当某个处理器修改了共享变量,需要缓存一致性协议来保证其他处理器也看到修改后的值。缓存一致性协议会在读操作后和写操作前加入缓存刷新操作,保证其他处理器的缓存是最新值。

3. volatile 的使用案例

volatile 关键字常用在 DCL(Double Check Lock)单例模式中:

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;
    }
}

这里使用 volatile 是为了防止指令重排序,保证 instance 初始化后其他线程可以看到。

volatile 也常用在Interruptible线程中,实现线程的中断功能:

public class InterruptibleThread extends Thread {
    private volatile boolean interrupted = false;

    public void interrupt() {
        interrupted = true;
    }

    @Override
    public void run() {
        while (!interrupted) {
            // do something
        }
    }
}

这里 volatile 可以保证 interrupted 的可见性,使线程立即响应中断调用。

4. volatile 的原子性问题

volatile 关键字只能保证可见性和有序性,不能保证原子性。

对一个 volatile 变量的读写操作并不是原子的,而是可以分为读、改、写三个操作:

  • 读: 读取 volatile 变量的值
  • 改:对值进行修改
  • 写:将修改后的值写入 volatile 变量

这三个操作并不是一个原子操作,在多线程环境下可能导致数据竞争问题:

public class VolatileNoAtomicDemo {
    private volatile int counter = 0;

    public void increase() {
        counter++;  // 不是原子操作
    }
}

这里的 counter++ 实际上分为三步:

  1. 读:读取 counter 的值,假设为 x
  2. 改:x + 1
  3. 写:将 x + 1 的结果写入 counter

在多线程环境下,如果两个线程同时执行 increase 方法,很有可能达不到预期结果,这就是因为 counter++ 不是一个原子操作导致的。

5. 如何解决 volatile 的原子性问题

要解决 volatile 的原子性问题,可以使用 synchronized 或 Atomic 包中的类。

使用 synchronized:

public synchronized void increase() {
    counter++;  
}

使用 AtomicInteger:

private AtomicInteger counter = new AtomicInteger(0);

public void increase() {
    counter.getAndIncrement();
}

AtomicInteger 中的方法都是原子操作,可以解决 volatile 的原子性问题。

synchronized 会影响性能,AtomicInteger 的性能更好,所以一般优先选择 Atomic 包中的原子类。

6. volatile 的实现原理

volatile 的实现原理依赖于 JMM(Java Memory Model)中的几个概念:

  • 主内存:所有线程都可以访问的内存,存储共享变量的值。
  • 工作内存:每个线程私有的内存,用于存储线程使用的变量值。
  • 内存屏障:控制读写的顺序,用于保证特定操作的完成后才允许执行后续操作。

volatile 的实现原理是:

  1. 当一个线程修改一个volatile变量的值时,它会在变量修改后立即刷新回主内存。
  2. 当一个线程读取一个volatile变量的值时,它会直接从主内存读取,而不是从工作内存读取。
  3. 它会在读后和写前加入内存屏障,以保证指令重排不会将内存操作重排到屏障另一侧。

这样就实现了:

  • 可见性:因为每次直接读写主内存,所以每个线程都可以获得最新值。
  • 有序性:内存屏障会阻止重排,读写顺序由代码决定。
  • 禁止编译器优化:因为每次都要从主内存读写,编译器难以对其进行优化。

JMM的这几个概念配合volatile关键字的实现原理,就保证了多线程环境下volatile变量的可见性、有序性和禁止编译器优化。

7. 小结

  • volatile关键字主要保证可见性、有序性和禁止编译器优化。
  • volatile的底层原理是依赖内存屏障和缓存一致性协议实现的。
  • volatile不能保证原子性,要配合synchronized或Atomic类解决。
  • volatile的实现依赖JMM中的主内存、工作内存和内存屏障等概念。

8. volatile的最佳实践

根据volatile的特性,我们可以总结出一些最佳实践:

  1. 不要过度使用volatile

    volatile关键字会影响程序性能,所以不要过度使用,只在真正需要可见性和有序性保证的地方使用。

  2. 与synchronized一起使用

    当需要保证原子性时,volatile关键字需要与synchronized关键字一起使用。synchronized可以保证代码块的原子性,volatile可以保证数据的可见性。

  3. 使用Atomic类代替synchronized和volatile

    Atomic类提供的方法都是原子操作,性能比synchronized更好,同时可以保证可见性,所以在需要保证原子性的场景可以优先选择Atomic类。

  4. 禁止把long和double类型变量声明为volatile

    根据JMM规范,对64位数据类型的读写操作不一定是原子的,所以不要将long和double类型的变量声明为volatile。可以使用AtomicLong和AtomicDouble类代替。

  5. volatile不保证顺序

    volatile关键字只能保证有序性,不能保证顺序。有序性是指:在一个线程内,不会由于编译器优化和处理器重新排序,使得对一个volatile变量的写操作排在读操作之前。顺序是指:两个线程访问同一个变量的顺序。所以不要依赖volatile保证线程间的顺序。

  6. volatile变量不能保护其它非volatile变量

    在使用volatile变量控制住多线程变量的可见性时,不要认为它可以保护其它非volatile变量。每个变量都需要单独使用volatile或synchronized来保护。

9. 案例:使用volatile实现双重检查锁定

双重检查锁定(Double Check Locking)是一种使用同步控制并发访问的方式,可以实现延迟初始化。它通过两次对对象引用进行空检查来避免同步,从而提高性能。

但是在Java中,普通的双重检查锁定是不起作用的,原因是有指令重排的存在,可能导致另一个线程看到对象引用不是null,但是对象资源还没有完成初始化。

使用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;
    }
}

这里把instance变量声明为volatile,可以禁止指令重排,保证在对象完成初始化后,其他线程可以正确看到instance不为null。

这种方式是实现Singleton模式的最佳方式,它只有第一次调用getInstance方法时才会同步,这样既可以实现线程安全,又有很高的性能。

10. 案例:使用volatile实现中断机制

我们可以使用一个volatile变量作为中断标志,在循环体内检查这个变量,一次循环检查后立即重新读取变量的值,保证对变量修改的可见性,从而实现中断机制。

public class VolatileInterruptionDemo extends Thread { 
    private volatile boolean interrupted = false;

    @Override
    public void run() {
        while (!interrupted) {
            // do something
        }
        System.out.println("Interrupted!");
    }

    public void interrupt() {
        interrupted = true;
    }
}

这里的interrupted变量被声明为volatile,可以保证线程可以感知到中断信号,从循环体内退出。

这就是使用volatile实现的一种简单的中断机制,利用了volatile的可见性来保证线程可以正确读取到最新的中断标志。

11. 案例:使用AtomicInteger代替volatile

前面提到过,volatile不能保证原子性,要解决这个问题可以使用synchronized或Atomic类。这里我们通过一个例子来展示如何使用AtomicInteger代替volatile。

先看一个使用volatile的例子:

public class VolatileDemo {
    private volatile int counter = 0;

    public void increase() {
        counter++;
    }

    public int getCounter() {
        return counter;
    }
} 

这里的counter++不是一个原子操作,在多线程环境下会存在数据竞争问题。

现在使用AtomicInteger代替:

public class AtomicDemo {
    private AtomicInteger counter = new AtomicInteger(0);

    public void increase() {
        counter.getAndIncrement();
    }

    public int getCounter() {
        return counter.get();
    }
}

AtomicInteger的getAndIncrement()方法是一个CAS原理的原子操作,可以保证线程安全。

AtomicInteger使用CAS操作实现原子操作,CAS操作包含三个操作:

  1. 获取变量的当前值V
  2. 对V的值进行操作
  3. 使用CAS操作设置变量的值,这个设置值的操作需要提供变量的当前值V和新值,当变量的当前值还是V时才会设置新值,否则重新获取当前值。

CAS操作可以保证如果在多个线程同时使用一个变量时,只有一个线程可以更新变量的值,其他线程的设置值操作都会失败,这种机制可以实现原子操作。

所以,通过这个例子我们可以看出,AtomicInteger是一个很好的替代volatile的选择,它可以保证原子性也具有volatile所有特性,性能也更好,是实现原子操作的最佳选择。

12. 案例:基于volatile实现一个简单的并发容器

这里我们实现一个简单的线程安全的容器,它只包含两个方法:add()和size()。

使用volatile和synchronized实现如下:

public class VolatileContainer {
    private volatile int size = 0;
    private Object[] items = new Object[10];
    
    public void add(Object item) {
        synchronized (items) {
            items[size] = item;
            size++;
        }
    }
    
    public int size() {
        return size;
    }
}

这里使用volatile声明size变量来保证线程安全,同时使用synchronized对items数组加锁来保证添加操作的原子性。

size()方法只需要简单的读取size变量,由于它被声明为volatile,可以保证每次得到的都是最新大小值。

这是一个使用volatile和synchronized实现的简单线程安全容器,利用了volatile的可见性和synchronized的互斥锁来保证线程安全。

相比直接对整个方法加锁,这种方式的性能会更好,因为size()方法没有加锁,可以并发执行,只有在必要的add()方法进行同步,这也体现了锁的精确性原则。

13. 小结

通过这几个案例,加深了对volatile和AtomicInteger的理解,主要体会到:

  1. volatile可以保证可见性和有序性,但不能保证原子性,要用synchronized或Atomic类补充。
  2. AtomicInteger可以完全替代volatile,并且性能更好,是原子操作的最佳选择。
  3. 合理使用volatile和锁可以实现较高性能的线程安全程序。锁的使用要遵循精确性原则,不要过度使用。
  4. volatile和AtomicInteger都是JMM的重要组成部分,理解它们的实现原理有助于使用它们。

14. 案例:使用AtomicStampedReference实现ABA问题的解决

ABA问题是这样的:如果一个变量V初次读取的值是A,它的值被改成了B,后来又被改回为A,那些个依赖于V没有发生改变的线程就会产生错误的依赖。

这个问题通常发生在使用CAS操作的并发环境中,我们可以使用版本号的方式来解决这个问题,每次变量更新的时候版本号加1,那么A->B->A这个过程就会被检测出来。

AtomicStampedReference就是用过这个原理来解决ABA问题的,它包含一个值和一个版本号,我们可以这样使用:

AtomicStampedReference<Integer> atomicRef = 
    new AtomicStampedReference<>(100, 0);

// 获取当前值和版本号  
int stamp = atomicRef.getStamp();
int value = atomicRef.getReference();

// 尝试设置新值和版本号
boolean success = atomicRef.compareAndSet(value, 101, stamp, stamp + 1);
if(success) {
    // 设置成功,获取新版本号
    stamp = atomicRef.getStamp(); 
}   

这里当我们重新设置值100的时候,由于版本号已经变了,所以compareAndSet会失败,ABA问题就被解决了。

AtomicStampedReference是JUC包中用来解决ABA问题的重要工具类,实际项目中也广泛使用,它利用版本号的方式巧妙解决了这个并发编程中容易产生的问题。

另外,AtomicStampedReference的版本号使用的是int类型,所以在高并发场景下也可能存在循环的问题,这个时候可以使用时间戳方式生成版本号来避免,不过一般情况下AtomicStampedReference已经可以很好解决ABA问题。

15. 总结

OK,到这里volatile相关内容就全部介绍完了,包括:

  1. volatile的定义及作用:可见性、有序性和禁止优化。
  2. volatile的底层实现原理:JMM、缓存一致性协议和内存屏障。
  3. volatile的使用实例:双重检查锁定和中断机制等。
  4. 如何解决volatile的原子性问题:使用synchronized和Atomic类。
  5. AtomicStampedReference用法和ABA问题解决。
  6. 一些volatile的最佳实践。
  7. 使用volatile和锁实现的一个简单线程安全容器。

讲解的内容比较广泛,试着结合理论和实践的方式进行解释,希望可以对大家理解volatile和并发编程有所帮助。这也是我学久而久之总结的一些心得体会,与大家共同分享学习。如果 对volatile和JMM还有哪些不理解的地方,也欢迎留言讨论,我们共同进步!再次感谢阅读这篇博客,也希望您能够在学习和工作中很好地应用volatile关键字!文章来源地址https://www.toymoban.com/news/detail-432462.html

到了这里,关于volatile关键字原理的使用介绍和底层原理解析和使用实例的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • volatile关键字作用

    volatile是一个和多线程相关的,主要有一下2点作用(只保证可见性,不保证原子性) 防止指令重排(有序性) JVM在不改变程序执行结果的前提下,在编译时会对指令的顺序进行重新排序,而volatile则能够禁止指令的重新排序 能够确保线程内存中的对象对其他内存可

    2024年02月15日
    浏览(47)
  • volatile 关键字详解

    目录 volatile volatile 关键用在什么场景下: volatile 防止编译器优化: volatile   是一个在许多编程语言中(包括C和C++)用作的标识符。它用于告诉编译器不要对带有该修饰的变量进行优化,以确保变量在特定情况下的可见性和预测性。 在C和C++中, volatile

    2024年02月11日
    浏览(44)
  • 【多线程】volatile关键字

    一、volatile 1.volatile的底层原理是内存屏障,Memory Barrier, Memory Fence 2.对volatile变量的写指令(赋值操作)后会加入写屏障 3.对volatile变量的读指令(取变量值)前会加入读屏障 4.写屏障的作用会将写屏障之前的赋值改动操作,对共享变量的改动都同步到主内存中 5.读屏障的作

    2024年02月06日
    浏览(43)
  • JAVA volatile 关键字

    volatile 是JAVA虚拟机提供的轻量级的同步机制,有三大特性 1、保证可见性  2、不保证原子性  3、禁止指令重排 JMM  JAVA内存模型本身是一种抽象的概念并不真实存在 它描述的是一组规则或规范,提供这组规范定义了程序中各个变量(包括实例变量、静态变量)的访问方式。

    2024年02月13日
    浏览(52)
  • [JAVAee]volatile关键字

    目录 1.volatile的特性 ①保持线程可见性 2.volatile注意事项及适用场景 ①注意事项 ②适用场景 volatile,译为\\\"易变的\\\". 对此我们就可以这样理解,对于被volatile修饰的变量的数值,是容易变化的. 在之前的线程安全文章中,我们有讲解过\\\"可见性\\\",对于线程间的这个特性可能会导致:线程

    2024年02月16日
    浏览(36)
  • 【C】volatile 关键字

    1)基本概念 const 是C语言的一个。 const 用于告诉编译器相应的变量可能会在程序的控制之外被修改,因此编译器不应该对其进行优化。 声明语法: 作用: 防止编译器优化,确保对变量的每次访问都是实际的读写操作,而不是使用缓存中的值。 用于表示可能会 被异步

    2024年01月22日
    浏览(42)
  • 【Java基础】volatile关键字

    关于作者:CSDN内容合伙人、技术专家, 从零开始做过日活千万级APP。 专注于分享各领域原创系列文章 ,擅长java后端、移动开发、人工智能等,希望大家多多支持。 我们继续总结学习Java基础知识,温故知新。 volatile 是一个Java,可以用来修饰变量,volatile也被称为轻

    2024年02月11日
    浏览(46)
  • C语言volatile关键字

    在C语言中, volatile 是一个类型修饰符,用于告诉编译器对象的值可能会在编译器无法检测到的情况下被改变。这通常发生在以下两种情况: 硬件的输入/输出操作,例如一个设备寄存器的读取或写入。 共享内存的并行程序,其中一个线程修改了一个内存位置,而另一个线程

    2024年02月07日
    浏览(54)
  • 浅析Java中volatile关键字

            Java中的volatile用于修饰一个变量,当这个变量被多个线程共享时,这个变量的值如果发生更新,每个线程都能获取到最新的值。volatile在多线程环境下还会禁止指令重排序,确保变量的赋值操作按照代码的顺序执行。需要注意是它不能保证变量操作的

    2024年01月21日
    浏览(49)
  • volatile关键字(轻量级锁)

    目录 一、volatile出现背景 二、JMM概述 2.1、JMM的规定  三、volatile的特性 3.1、可见性  3.1.1、举例说明  3.1.2、总结 3.2、无法保证原子性 3.2.1、举例说明 3.2.2、分析 3.2.3、使用volatile对原子性测试  3.2.4、使用锁机制  3.2.5、总结 3.3、禁止指令重排序  四、volatile的内存语义 4

    2024年02月15日
    浏览(41)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包