什么是 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修改了共享变量后,其他线程可以获取到最新的变量数据
代码案例
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,符合预期。
评论区