目 录CONTENT

文章目录

Java中的volatile关键字到底是个什么玩意儿?

小张的探险日记
2023-02-20 / 0 评论 / 0 点赞 / 489 阅读 / 4,691 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2023-02-20,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

什么是 volatile?

volatile是java 提供的最轻量级的同步机制,可以保证变量对多线程的可见性。

前置知识-Java 内存模型

Java内存模型(Java Memory Model)是Java虚拟机规范中定义的一种抽象的计算机内存模型,它规定了多线程之间共享数据的可见性、原子性和有序性等行为。Java内存模型定义了线程如何访问共享内存中的变量,以及如何同步多个线程之间的访问,从而保证程序的正确性和可靠性。Java内存模型将内存划分为主内存(Main Memory)和每个线程的本地内存(Thread Local Memory)。主内存是共享的,所有线程都可以访问,而每个线程都有自己的本地内存,线程中的变量存储在本地内存中。
Java内存模型规定了共享变量在主内存和本地内存之间的交互方式,主要包括以下几个方面:

  •     可见性:当一个线程修改了一个共享变量的值时,这个值对于其他线程是可见的。
  •     原子性:对于多个线程同时访问同一个变量,任何时刻只有一个线程能够修改该变量的值。
  •     有序性:线程之间的操作执行顺序是不确定的,但是对于每个线程内部的操作执行顺序是确定的。

Java提供了一些同步机制来保证共享变量在多线程之间的可见性、原子性和有序性,例如synchronized关键字、volatile关键字、Lock对象、Atomic类等。这些同步机制可以帮助开发者编写线程安全的程序,避免出现数据竞争等并发问题。

可见性

内存可见性问题是指,当一个线程修改了共享变量的值时,其他线程可能无法立即看到这个修改,因为它们可能在本地缓存中持有该变量的副本,而不是直接从内存中读取。这就导致了数据不一致的问题。

每个线程都有自己的工作内存,默认情况下不会直接读取主内存的数据,这也是线程之前的数据隔离,而 volatile是 C/C++ 中的一个关键字,用于告诉编译器和处理器不要对该变量进行优化和缓存,每次读写都要从主内存中进行,这样就保存线程 T1修改了共享变量后,其他线程可以获取到最新的变量数据

image.png

代码案例

stop 变量被标记为 volatile,这意味着它的写入和读取操作都不会被编译器优化,也就是说,任何对 stop 的修改都会立即同步到主内存,并且任何从主内存中读取 stop 的操作都会获取到最新的值。
所以当主线程去调用了 stop 方法后, stop 变量的值会直接同步到主内存,获取 stop 变量也会获取到最新的值,则会跳出循环。

public class VolatileVisibilityExample {
    private volatile boolean stop = false;
    public void run() {
        while (!stop) {
            // do something
        }
        System.out.println("Stopped.");
    }
    public void stop() {
        stop = true;
    }
    public static void main(String[] args) throws InterruptedException {
        VolatileVisibilityExample example = new VolatileVisibilityExample();
        new Thread(example::run).start();
        Thread.sleep(1000); // 等待 1 秒钟
        example.stop();
    }
}

volatile的指令重排

在编写并发程序时,编译器可能会对指令进行重排,以提高程序的性能。这种优化可能会在单线程程序中有效,但在多线程程序中却可能会导致意外的结果。
为了避免这种情况,可以使用 volatile 关键字。在 C 和 C++ 中,volatile 关键字告诉编译器,不要对该变量进行优化,因为它可能会被其他线程或外部因素修改。

使用 volatile 关键字可以禁止编译器对指令进行重排,并保证程序的正确性。然而,需要注意的是,volatile 并不能保证线程安全,因为它并不能解决多线程访问同一个变量时的竞争条件问题。在多线程编程中,应该使用锁或其他同步机制来确保线程安全。

关于volatile 的内存屏障

内存屏障(Memory Barrier)是一种同步机制,用于确保多线程程序中的共享变量能够被正确地读取和写入。内存屏障会强制刷新CPU的缓存,使得其缓存的数据与主内存中的数据同步。

在Java中,内存屏障分为四种类型:

Load Barrier(读屏障):确保读操作能够读取到最新的变量值,而不是之前的旧值。

Store Barrier(写屏障):确保写操作能够将最新的变量值写入主内存,并且其他线程能够看到这个新值。

Read Barrier(读写屏障):结合了读屏障和写屏障的功能,确保读操作和写操作的顺序被正确地保留。

Full Barrier(全屏障):结合了所有三种屏障的功能,确保所有的内存访问操作都按照程序中定义的顺序执行。

内存屏障在Java多线程编程中非常重要,可以保证多线程程序的正确性和可靠性。在使用多线程编程时,程序员需要了解内存屏障的作用和使用方法,以便正确地编写线程安全的程序。

内存屏障可以限制多核CPU中指令的执行顺序,从而保证共享变量的访问顺序是一致的。具体来说,内存屏障通常会在CPU指令执行的过程中进行控制,限制在指定的内存屏障之前的所有指令都已经执行完毕,并且其结果已经写回内存中,然后才会继续执行之后的指令。这样可以保证不同核心之间对共享变量的访问顺序是一致的,从而避免数据不一致的问题。

需要注意的是,内存屏障并不是完全限制CPU指令的执行顺序,因为现代CPU中通常会使用乱序执行(Out-of-Order Execution)等技术来提高指令执行效率。但是内存屏障可以限制一些重要的指令执行顺序,从而保证共享变量的访问顺序是一致的,这对于多线程编程非常重要。

因此,可以说内存屏障是通过限制多核CPU中指令的执行顺序来保证共享变量的访问顺序是一致的。

volatile 默认是读写屏障

主内存指的是内存条的内存空间

volatile 原子性

虽然volatile关键字可以确保对共享变量的读写操作对所有线程可见,但是它并不能保证原子性。
原子性是指一个操作是不可中断的,要么全部执行成功,要么全部不执行。如果多个线程同时对同一个变量进行写操作,就可能会出现竞态条件(Race Condition),导致数据不一致的问题。

举个例子,假设有一个共享变量count,初始值为0,多个线程同时对其进行加1操作,代码如下:

volatile int count = 0;
// 线程1代码
count++;
// 线程2代码
count++;

由于volatile只能保证变量的内存可见性,当线程1和线程2同时执行count++时,就可能会出现竞态条件,导致最终的结果不是2。

使用 Atomic类解决并发问题

为了保证原子性,需要使用更强的同步机制,例如synchronized关键字或者Java并发包中的Atomic类。例如,可以将count声明为AtomicInteger类型,代码如下:

AtomicInteger count = new AtomicInteger(0);
// 线程1代码
count.incrementAndGet();
// 线程2代码
count.incrementAndGet();


这样,通过AtomicInteger提供的incrementAndGet()方法,可以保证对count的操作是原子性的,不会出现竞态条件。

使用Lock解决volatile变量锁竞争

获取、修改 时都上锁,保证同一时间只有一个线程能够访问到该变量,其余线程阻塞

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class VolatileLockExample {
    private volatile int volatileVariable = 0;
    private Lock lock = new ReentrantLock();
    public void updateVolatileVariable() {
        lock.lock();
        try {
            volatileVariable++;
        } finally {
            lock.unlock();
        }
    }
    public int getVolatileVariable() {
        lock.lock();
        try {
            return volatileVariable;
        } finally {
            lock.unlock();
        }
    }
    public static void main(String[] args) {
        VolatileLockExample example = new VolatileLockExample();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                example.updateVolatileVariable();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                example.updateVolatileVariable();
            }
        });
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(example.getVolatileVariable()); // 输出 200000
    }
}

使用 synchronized解决并发问题

举个例子,假设有一个共享变量count,初始值为0,多个线程同时对其进行加1操作,可以使用synchronized来保证原子性,代码如下:

class Counter {
    private int count = 0;
    public synchronized void increment() {
        count++;
    }
    public int getCount() {
        return count;
    }
}

// 创建一个Counter对象
Counter counter = new Counter();

// 创建多个线程同时对count进行操作
Thread thread1 = new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        counter.increment();
    }
});
Thread thread2 = new Thread(() -> {
    for (int i = 0; i < 1000; i++) {
        counter.increment();
    }
});

// 启动线程
thread1.start();
thread2.start();

// 等待线程执行完成
thread1.join();
thread2.join();

// 输出最终结果
System.out.println(counter.getCount()); // 输出2000

在Counter类中,使用synchronized关键字修饰了increment()方法,保证了对count的操作是原子性的。当多个线程同时调用increment()方法时,会互斥地获取Counter对象的锁,确保每次只有一个线程可以执行该方法。

在main函数中,创建了多个线程同时对Counter对象进行操作,通过join()方法等待线程执行完成后,输出最终结果。由于使用了synchronized关键字保证了对count的操作是原子性的,因此最终输出的结果是2000,符合预期。

0

评论区