被 volatile 修饰的实例变量或者类变量有如下两层含义:
package concurrent.volatileDemo;
import java.util.concurrent.TimeUnit;
public class VolatileFoo {
// init_value 的最大值
final static int MAX = 5;
// init_value 的初始值
// 使用 volatile
static volatile int init_value = 0;
public static void main(String[] args) {
// 启动一个 Reader 线程,当发现 local_value 和 init_value 不同时,则输出 init_value 被修改的信息
new Thread(() -> {
int localValue = init_value;
while (localValue < MAX) {
if (init_value != localValue) {
System.out.printf("The init_value is update to [%d]\n", init_value);
// 对 localValue 进行重新赋值
localValue = init_value;
}
}
}, "Reader").start();
// 启动 uddater 线程,主要用于对 init_value 的修改,当 local_value >= 5 时,退出
new Thread(() -> {
int localValue = init_value;
while (localValue < MAX) {
// 修改 init_value
System.out.printf("The init_value will be changed to [%d]\n", ++localValue);
init_value = localValue;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "Updater").start();
}
}
Updater 线程对 init_value 变量的每一次更改都会使得 Reader 线程能够看到,其具体步骤如下。
1 Reader 线程从主内存中获取 init_value 的值为0,并且将其缓存到本地工作内存中。
2 Updater 线程将 init_value 的值在本地修改为1,然后立即刷新到主内存。
3 Reader 线程在本地工作内存中 init_value 失效。
4 由于 Reader 线程工作内存中的 init_value 失效,因此需要到主内存中重新读取 init_value 的值。
volatile 关键值直接禁止 JVM 和处理器对 volatile 关键字修饰的指令重排序,但是对于 volatile 前后无依赖关系的指令则可以随意怎么排序。
int x = 0;
int y = 1;
volatile int z = 20;
x++;
y--;
在语句 volatile int z = 20; 之前,先执行 x 的定义还是先执行 y 的定义,并不关心,只要能够百分百地保证在执行到 z=20 的时候 x=0,y=1,同理关于 x 的自增以及 y 的自减操作都必须在 z = 20 以后才发生。
再看下面这个例子,当 initialized 增加了 volatile 的修饰,那就意味着 initialized = true; 一定在 context = loadContext(); 之后执行。
private volatile boolean initialized = false;
private Context context;
public Context load(){
if(!initialized){
context = loadContext();
initialized = true; // 阻止重排序
}
return context;
}
package concurrent.volatileDemo;
import java.util.concurrent.CountDownLatch;
public class VolatileTest {
// 使用 volatile 修饰共享资源 i
private static volatile int i = 0;
private static final CountDownLatch latch = new CountDownLatch(10);
public static void inc() {
i++;
}
public static void main(String[] args) {
// 计数器为10
CountDownLatch countDownLatch = new CountDownLatch(10);
// 将 CountDownLatch 对象传递到线程的 run() 方法中,当每个线程执行完毕 run() 后就将计数器减1
MyThread myThread = new MyThread(countDownLatch);
long start = System.currentTimeMillis();
// 创建 10 个线程,并执行
for (int i = 0; i < 10; i++) {
new Thread(myThread).start();
}
try {
// 主线程(main)等待:等待的计数器为0;即当 CountDownLatch 中的计数器为0时,Main 线程才会继续执行。
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println(i);
}
}
class MyThread implements Runnable {
private CountDownLatch latch;
public MyThread(CountDownLatch latch) {
this.latch = latch;
}
@Override
public void run() {
try {
for (int i = 0; i < 1000; i++) {
VolatileTest.inc();
}
} finally {
latch.countDown(); // 每个子线程执行完毕后,触发一次 countDown(),即计数器减1
}
}
}
上面这段代码创建了10个线程,每个线程执行 1000 次对共享变量 i 的自增操作,但是最终结果肯定不是 10000,而且每次运行的结果也各不相同。原因分析如下:
i++ 的操作其实是由三步组成的,具体如下:
1)从主内存中获取 i 的值,然后缓存到线程工作内存中。
2)在线程工作内存中为 i 进行加 1 的操作。
3)将 i 的最新值写入主内存中。
上面三个操作单独的每一个操作都是原子性操作,但是合起来就不是,因为在执行的过程中可能被其他线程打断,例如如下操作情况。
1)假设某个时刻 i 的值为 100,线程 A 要对变量 i 执行自增操作,首先它需要到主内存中读取 i 的值,可是此时由于 CPU 时间片调度的关系,执行权切换到线程 B,A 线程进入了 RUNNABLE 状态而不是 RUNNING 状态。
2)线程 B 同样需要从主内存中读取 i 的值,由于线程 A 没有对 i 做过任何修改操作,因此此时 B 获取到的 i 仍然是 100。
3)线程 B 工作内存中为 i 执行了加 1 操作,工作内存中变成 101,但是未刷新到主内存。
4)CPU 时间片调度又将执行权交给了线程 A,A 线程对工作线程中的 100 进行了加 1 运算,工作内存中变成 101。
5)线程 A 将 i=101 写入主内存中。
6)线程 B 将 i=101 写入主内存中。
这样两次运算实际上只对 i 进行了一次修改变化。
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/chengqiuming/article/details/124055342
内容来源于网络,如有侵权,请联系作者删除!