1.引言
大家好!今天我来和大家聊一聊一个在Java面试中常常出现的经典问题——线程B怎么知道线程A修改了变量?
这个问题非常典型,面试官常常用这个问题来考察候选人对Java线程之间通信和共享数据的理解。作为一个资深程序员,我也常常遇到这个问题,今天就来和大家分享一下解决方案。接下来,我们会从四个方面详细探讨这个问题:volatile修饰变量、synchronized修饰修改变量的方法、wait/notify以及while轮询。让我们从一个简单的故事开始,一步步解开这个谜题。
图片
2.线程 A 修改了变量,线程 B 怎么知道?
假设你和你的同事在一个项目中负责不同的任务,你负责A模块,他负责B模块。你们俩需要共享一些数据,每当你修改了A模块的数据时,B模块应该能够及时感知到这个变化并做出反应。但如果不加任何同步机制,这种数据的共享就变得非常麻烦,因为Java内存模型的存在,使得A线程修改的变量可能并不会立刻反映到B线程的视野中。
问题:线程A修改了变量X,但线程B不一定能立即看到X的变化,这个时候,B该怎么办呢?接下来,我将从四个不同的角度,帮你全面理解这个问题。
3.使用volatile修饰变量
volatile是最常见的解决方案之一。它的作用是确保一个变量的修改对其他线程是立即可见的。简单来说,当线程A修改了某个被volatile修饰的变量时,线程B能够立刻看到变量的变化。
那么为什么会有这个效果呢?
当你在一个变量前加上volatile关键字时,它的变化会被写入主内存,而不是保存在线程的本地缓存中。这样,当线程B去读取这个变量时,它会直接从主内存读取,而不是从缓存中读取,因此B总能看到A线程对变量的最新修改。
代码示例:
图片
在这个例子中,线程A修改了flag变量,而线程B则通过while循环不断检测flag的值。当线程A修改了flag时,由于flag被volatile修饰,线程B能立刻看到变化。
总结:volatile关键字确保了对变量的修改对其他线程是立刻可见的,但它仅仅适用于一些简单的共享变量场景。对于复杂的共享状态,它并不适用。
4.使用synchronized修饰修改变量的方法
我们知道,synchronized是Java中用于处理线程同步的关键字,能够保证在同一时刻只有一个线程执行被修饰的方法或代码块。当一个线程获得了某个对象的锁后,其他线程就必须等待该锁释放才能继续执行。这对于解决线程之间的共享数据问题非常有效。
代码示例:
图片
在这个例子中,increment方法被synchronized修饰,确保每次只有一个线程可以修改count。线程B在读取count时需要获取锁,确保它读取的值是最新的。
总结:synchronized关键字不仅保证了对共享资源的同步访问,也确保了线程B能够读取到线程A修改后的最新变量。
5.使用wait/notify实现线程间通信
在多线程编程中,wait和notify是一对非常有用的工具,可以实现线程间的等待和通知机制。这是一种更加灵活的线程间通信方式,能够让一个线程在满足某些条件时等待,而另一个线程则可以通过notify或notifyAll来通知正在等待的线程。
代码示例:
图片
在这个例子中,线程A修改了flag后,通过notify方法通知线程B,而线程B则通过wait进入等待状态,直到flag的值被修改为true。
总结:使用wait/notify机制,线程间的通信更加高效,线程B能在适当的时机及时感知线程A的状态变化。
6.使用while轮询
最后,我们来看一下while轮询的方式。虽然它不像volatile、synchronized和wait/notify那样显得优雅和高效,但它在某些简单的场景中仍然有效。
在这种方法中,线程B通过一个while循环不断地检查某个条件是否满足,直到条件满足为止。需要注意的是,while轮询很容易导致CPU的浪费,因此通常需要配合Thread.sleep()来减少不必要的资源占用。
代码示例:
图片
总结:while轮询是一种简单的方式,但它不如volatile和synchronized那样高效,且容易造成性能问题。
END
通过这四种方式:volatile、synchronized、wait/notify和while轮询,我们可以让线程B及时知道线程A修改了变量。每种方式都有其优缺点,选择合适的方式取决于具体场景的需求。