【Java|多线程与高并发】volatile关键字和内存可见性问题

简介: synchronized和volatile都是Java多线程中很重要的关键字,但它们的作用和使用场景有所不同。

1.前言

synchronized和volatile都是Java多线程中很重要的关键字,但它们的作用和使用场景有所不同。


synchronized关键字可以保证同一时刻只有一个线程可以访问被synchronized关键字保护的代码块,从而避免多个线程对共享资源的并发访问导致的数据不一致问题。


关于synchronized关键字更详细的介绍,可以参考我之前写的这篇文章线程安全问题以及synchronized使用实例


volatile用于保证变量在多个线程之间的可见性和有序性。


本文主要介绍valatite关键字

ef96c7ced0464ab8a140e26af49d324b.gif



在介绍volatile关键字之前,先来认识一下编译器优化带来的问题.


2. 编译器优化带来的内存可见性问题

编译器优化是编译器在编译源代码时,对代码进行的一系列优化处理,以提高程序的运行效率和性能。


编译器优化的主要目标是在不改变程序功能的前提下,尽可能地减少程序的运行时间和内存占用。


但编译器优化在多线程环境下可能会造成内存可见性问题.


内存可见性问题:

当一个线程修改一个共享变量的值时,这个值会被保存在该线程的本地内存中,而不是直接写入主内存中。

如果其他线程需要读取该共享变量的值,它们可能会从自己的本地内存中读取旧值,而不是从主内存中读取最新的值,从而导致数据不一致的问题。


示例:


public class Demo9 {
    static class Counter{
        public  int count = 0;
    }
    public static void main(String[] args) {
        Counter counter = new Counter();
        Thread t1 = new Thread(()->{
            while(counter.count == 0){
            }
            System.out.println("t1 执行结束!");
        });
        t1.start();
        Thread t2 = new Thread(()->{
            Scanner scanner = new Scanner(System.in);
            System.out.print("> ");
            counter.count = scanner.nextInt();
        });
        t2.start();
    }
}

运行结果:


069199dbc5264a8ba0a81ff1e0e15513.png

虽然counter.count的值修改成1了,但是t1的循环并没有结束. 为什么呢?


其实原因主要是在这里


0ffb411ef7684d0a95d3ec39558363fb.png

counter.count == 0 这个操作会有两步,读内存(load)和进行比较(cmp).加上这里的条件是while,那么此时读内存和进行比较就会执行很多次.


编译器就会对上述代码进行优化,读内存比进行比较这个操作慢得多.

既然频繁读内存,且每次读内存后的值都是一样的,那么就没必要多次读内存了.只读一次后面就直接读本地内存中的值(提高效率).


因此在进行修改counter.count的值之前,t1线程就已经读过counter.count的值了,t2修改了但t1并没有感知到. 这也就是编译器优化带来的内存可见性问题


3. 使用volatile保证内存可见性

内存可见性是指多个线程之间共享变量时,对变量的修改能够被其他线程及时地看到。


当一个变量被声明为volatile时,每次读取该变量时,都会从主内存中读取最新的值,而不是从线程的本地内存中读取。同样,每次写入该变量时,都会立即将值刷新到主内存中,而不是仅在线程的本地内存中修改。


接下来就可以通过volatile关键字解决上述的问题

8bc40489632c45c6a8634298c29ffe8a.png


只需在count变量前加上volatile即可.


运行结果:


28a9b076ee2d46b29293850b605edbaa.png

可以看到加上volatile关键字之后,修改count的值,t1就能够"感知"到了.


上述就是简单的使用volatile保证内存可见的简单案例


但其实编译器优化导致的内存可见性问题,也并不是一定就会发生.


4d41c5daba544fe7ad847ea70d77fcad.png

如果让t1线程这里的while里面加一个线程休眠2s这段代码,此时即使不加volatile关键字,也不会导致内存可见性问题.


02ecbacc2bae4c408215b70c8082510a.png


这里为什么不会产生内存可见性呢?


2s对于我们人来说,很短.但是对于计算机来说却是很漫长的.

别忘了编译器优化的目的,编译器优化主要是为了提高效率.

休眠2s,编译器即使优化了也没有什么提升.


5.volatile不能保证原子性

原子性是指操作或事务的不可分割性和不可中断性


就以两个线程针对同一个变量,同时进行修改操作为例:


class Counter{
    public volatile int count;
    public void add(){
        count++;
    }
}
public class Demo10 {
    private static Counter counter = new Counter();
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                counter.add();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = "+counter.count);
    }
}

运行结果:

04737d7ae9f545c7acc32d23735b729c.png


其实之前谈synchronized的时候,就说过这个问题.


count++这个操作可以分为3步:

961b653319654da98ae910b00530b654.png


因此count++并不是原子性. 我们需要使用synchronized来保证原子性.但是volatile并不能保证原子性


以JMM的角度看待volatile

JMM是Java内存模型(Java Memory Model)的简称,是一种规范,用于规定Java虚拟机(JVM)如何与计算机内存交互,以及多线程如何访问共享内存。


volatile禁止了编译器优化,避免了直接读取缓存(工作内存)中的数据,而是每次都去读取主内存中的数据.


以JMM的视角看待volatile:

正常程序运行时,会把主内存中的数据加载到工作内存中,在进行计算处理.

编译器优化可能会导致读到的数据来自于工作内存,而不是主内存

volatile的效果就是保证每次读到的数据都是从主内存读到的


总结

volatile关键字主要用于保证可见性和有序性,但不能保证原子性。

适用场景:


1.变量被多个线程共享,且其中一个线程修改了该变量的值,需要让其他线程立即看到该修改。

2.变量的值在程序中的读写顺序很重要,需要保证操作的有序性。

volatile会禁止编译器和JVM对代码进行优化,增加了内存的读写操作,降低了程序的执行效率。

2952e8638c074f8eae199834310ed740.gif


相关文章
|
1天前
|
缓存 监控 Java
Java一分钟之-Apache Geode:分布式内存数据平台
【5月更文挑战第21天】Apache Geode是低延迟的分布式内存数据平台,用于构建实时应用,提供缓存、数据库和消息传递功能。本文聚焦于Geode的常见问题,如数据一致性(数据同步延迟和分区冲突)和性能瓶颈(网络延迟和资源管理不当),并提出解决方案。确保数据一致性可通过选择合适的数据策略和利用`InterestPolicy`、`CacheListener`;提升性能则需优化网络和合理配置资源。通过示例代码展示了如何创建和操作Geode的Region。正确配置和调优Geode对于实现高可用、高性能应用至关重要。
16 1
|
1天前
|
消息中间件 Java Linux
2024年最全BATJ真题突击:Java基础+JVM+分布式高并发+网络编程+Linux(1),2024年最新意外的惊喜
2024年最全BATJ真题突击:Java基础+JVM+分布式高并发+网络编程+Linux(1),2024年最新意外的惊喜
|
1天前
|
安全 Java 程序员
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第20天】本文将深入探讨Java并发编程的核心概念,包括线程安全和性能优化。我们将详细解析synchronized关键字、ReentrantLock类以及java.util.concurrent包中的高级工具类,如Semaphore、CountDownLatch和CyclicBarrier等。通过实例演示如何使用这些工具来提高多线程程序的性能和可靠性。
|
1天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第20天】 在多核处理器日益普及的今天,并发编程成为了软件开发中不可忽视的重要话题。Java语言提供了丰富的并发工具和机制来帮助开发者构建高效且线程安全的应用程序。本文将探讨Java并发的核心概念,包括线程同步、锁机制、以及如何通过这些工具实现性能优化。我们将透过实例分析,揭示并发编程中的常见问题,并展示如何利用现代Java API来解决这些问题。
|
1天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第20天】在Java并发编程中,线程安全和性能优化是两个关键要素。本文将深入探讨Java并发编程的基本概念、线程安全的实现方法以及性能优化技巧。通过分析同步机制、锁优化、无锁数据结构和并发工具类的使用,我们将了解如何在保证线程安全的前提下,提高程序的性能。
|
2天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第20天】 在Java开发中,正确处理并发问题对于确保应用的稳定性和提高性能至关重要。本文将深入探讨Java并发编程的核心概念——线程安全,以及如何通过各种技术和策略实现它,同时保持甚至提升系统性能。我们将分析并发问题的根源,包括共享资源的竞争条件、死锁以及线程活性问题,并探索解决方案如同步机制、锁优化、无锁数据结构和并发工具类等。文章旨在为开发者提供一个清晰的指南,帮助他们在编写多线程应用时做出明智的决策,确保应用的高效和稳定运行。
|
2天前
|
安全 Java
Java中的多线程编程:概念、实现及性能优化
【5月更文挑战第20天】在计算机科学中,多线程是一种允许程序同时执行多个任务的技术。Java作为一种广泛使用的编程语言,提供了对多线程编程的支持。本文将介绍Java中多线程的基本概念、实现方法以及性能优化策略,帮助读者更好地理解和应用多线程技术。
|
3天前
|
Java
Java一分钟之-并发编程:线程间通信(Phaser, CyclicBarrier, Semaphore)
【5月更文挑战第19天】Java并发编程中,Phaser、CyclicBarrier和Semaphore是三种强大的同步工具。Phaser用于阶段性任务协调,支持动态注册;CyclicBarrier允许线程同步执行,适合循环任务;Semaphore控制资源访问线程数,常用于限流和资源池管理。了解其使用场景、常见问题及避免策略,结合代码示例,能有效提升并发程序效率。注意异常处理和资源管理,以防止并发问题。
25 2
|
3天前
|
安全 Java 容器
Java一分钟之-并发编程:线程安全的集合类
【5月更文挑战第19天】Java提供线程安全集合类以解决并发环境中的数据一致性问题。例如,Vector是线程安全但效率低;可以使用Collections.synchronizedXxx将ArrayList或HashMap同步;ConcurrentHashMap是高效线程安全的映射;CopyOnWriteArrayList和CopyOnWriteArraySet适合读多写少场景;LinkedBlockingQueue是生产者-消费者模型中的线程安全队列。注意,过度同步可能影响性能,应尽量减少共享状态并利用并发工具类。
17 2
|
3天前
|
Java 程序员 调度
Java中的多线程编程:基础知识与实践
【5月更文挑战第19天】多线程编程是Java中的一个重要概念,它允许程序员在同一时间执行多个任务。本文将介绍Java多线程的基础知识,包括线程的创建、启动和管理,以及如何通过多线程提高程序的性能和响应性。