目录
- 一、 volatile 是什么?
- 二、 volatile 解决了什么问题?
- 三、 怎么使用 volatile?
- 四、 volatile 的局限性(重要!)
- 五、 什么时候使用 volatile?
- 六、 volatile 在单例模式中的应用(双重检查锁)
- 七、总结
我的其他文章也讲解的比较有趣,如果喜欢博主的讲解方式,可以多多支持一下,感谢!
了解synchronized关键字请看: synchronized 关键字:线程同步的“VIP 包间”
今儿个就让我们来讲解一下Java中的volatile
关键字
你可以把 volatile
想象成一个“小喇叭” ,专门用来修饰 Java 中的变量。这个“小喇叭”的作用是:
volatile
修饰后,任何线程对这个变量的修改,都会立刻“广播”给所有其他线程。也就是说,其他线程会立刻知道这个变量的值变了,而不是用自己缓存的旧值。 volatile
可以阻止这种优化,保证代码按照你写的顺序执行。 在多线程环境下,每个线程都有自己的“小仓库”(工作内存),用来存放共享变量的副本。如果没有 volatile
,可能会出现以下问题:
volatile
就是为了解决这些问题而生的。它可以确保所有线程都能看到共享变量的最新值,避免数据不一致和程序出错。
举个例子:
想象一下,你和你的朋友们一起玩一个猜数字游戏。
volatile
: 你写下了一个数字,然后告诉你的朋友们。但是,你的朋友们可能没有立刻听到你的话,他们还在用自己之前猜的数字。这样,游戏就很难进行下去,因为大家用的数字不一样。 volatile
: 你写下了一个数字,然后用一个“大喇叭” 告诉你的朋友们。你的朋友们立刻就能听到你的话,知道你写下的数字是什么。这样,大家用的数字就是一样的,游戏就能顺利进行下去。 使用 volatile
很简单,只需要在变量声明的时候加上 volatile
关键字即可。
示例代码:
public class VolatileExample {
// 使用 volatile 修饰的变量
private volatile boolean running = true;
public void start() {
System.out.println("线程开始运行...");
while (running) {
// 执行一些操作
}
System.out.println("线程停止运行...");
}
public void stop() {
running = false; // 修改 running 的值
}
public static void main(String[] args) throws InterruptedException {
VolatileExample example = new VolatileExample();
// 启动一个线程
Thread thread = new Thread(example::start);
thread.start();
// 等待一段时间
Thread.sleep(1000);
// 停止线程
example.stop();
}
}
代码解释:
running
变量被 volatile
修饰,表示它是共享的,并且需要保证可见性。start()
方法在一个循环中执行一些操作,直到 running
变为 false
。stop()
方法将 running
设置为 false
,通知线程停止运行。main()
方法中,我们启动一个线程,然后等待一段时间,最后调用 stop()
方法停止线程。如果没有 volatile
:
线程可能永远不会停止,因为它可能一直在使用自己缓存的 running
值,而不知道 running
已经被修改为 false
了。
有了 volatile
:
当 stop()
方法将 running
设置为 false
时,这个修改会立刻被“广播”给所有线程,包括正在运行的线程。线程会立刻知道 running
变成了 false
,然后停止运行。
volatile
只能保证变量的可见性,不能保证原子性。 ⚠️
原子性: 一个操作要么全部完成,要么完全不完成,不会被其他线程中断。
举个例子:
volatile int count = 0;
public void increment() {
count++; // 这不是一个原子操作
}
count++
实际上包含了三个操作:
count
的值。count
的值加 1。count
。如果多个线程同时执行 increment()
方法,可能会出现以下情况:
count
的值为 0。count
的值为 0。count
的值加 1,然后写回 count
,count
的值为 1。count
的值加 1,然后写回 count
,count
的值为 1。(而不是期望的 2) ♀️这就是因为 count++
不是一个原子操作,多个线程同时执行时可能会互相干扰。
总结:
volatile
保证可见性,但不保证原子性。synchronized
关键字或者 java.util.concurrent
包中的原子类(如 AtomicInteger
)。 想象一下,你要创建一个“独一无二”的对象,就像一个班级里只有一个班长。这个“独一无二”的对象就是单例模式要实现的目标。
双重检查锁(Double-Checked Locking)就像一个“谨慎”的班长选举方法。它想尽量减少大家排队投票的时间,所以设计了一个“两次检查”的机制。
代码示例:
public class Singleton {
private volatile static Singleton instance; // 使用 volatile,很重要!
private Singleton() {
// 私有构造方法,防止别人自己选班长
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查:看看有没有班长了?
synchronized (Singleton.class) { // 只有没班长的时候,才需要排队选班长
if (instance == null) { // 第二次检查:排队的时候,可能别人已经选了班长,再确认一下
instance = new Singleton(); // 终于选出班长了!
}
}
}
return instance; // 返回班长
}
}
问题:如果没有 volatile
,选班长可能会出什么问题?
instance = new Singleton();
这行代码,看起来很简单,但实际上计算机要做好几件事:
Singleton
对象)。Singleton
对象)。instance
指向这个新班长。指令重排序捣乱:
如果没有 volatile
,计算机可能会偷懒,先“贴个公告”,再“给班长发教材”。 也就是说,先让 instance
指向了空教室,但教室里还没人,教材也没发。
线程安全问题:
instance
不为 null
了),但还没“发教材”。instance
不为 null
),就直接去“找班长”了。volatile
的作用:
volatile
就像一个“强制规定”,告诉计算机必须先“发教材”,再“贴公告”。 这样,即使线程 A 还没“发完教材”,线程 B 也不会看到“公告”,就不会拿到一个“空教室”了。
总结:
volatile
,计算机可能会偷懒,导致线程拿到一个“空教室”(未初始化的对象)。volatile
就像一个“强制规定”,保证计算机按照正确的顺序“选班长”,避免出现问题。更简洁的单例模式(推荐):
虽然双重检查锁是一种常见的单例模式实现方式,但它比较复杂,容易出错。更推荐使用静态内部类的方式来实现单例模式,这种方式更加简洁、安全,而且不需要使用 volatile
关键字。
public class Singleton {
private Singleton() {
// 私有构造方法,防止外部实例化
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
解释:
SingletonHolder
是一个静态内部类,只有在调用 getInstance()
方法时才会被加载。INSTANCE
是一个静态常量,在类加载时会被初始化,而且只会被初始化一次。volatile
是一个轻量级的同步机制,可以保证变量的可见性,但不能保证原子性。在多线程编程中,需要根据具体情况选择合适的同步机制。在双重检查锁的单例模式中,volatile
可以防止指令重排序,确保线程安全。但更推荐使用静态内部类的方式来实现单例模式,这种方式更加简洁、安全,而且不需要使用 volatile
关键字。
希望这篇文章能够帮助你理解 volatile
关键字! 记住,理解概念最重要,然后才能灵活运用。 加油!