点击 Mr.绵羊的知识星球 解锁更多优质文章。
一、介绍
1. 简介
volatile是java关键字,同时也是JVM 提供的轻量级的同步机制
2. 特性
你需要先了解一下Java内存模型Java Memory Model (JMM详解,写完上传),而volatile关键字拥有以下特性(不保证原子性),也就是说他无法保证线程安全。
(1) 保证可见性:git地址
可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* VolatileVisibility
* 保证可见性验证
*
* @author wxy
* @date 2023-02-23
*/
public class VolatileVisibility extends AbstractVolatile {
private static final Logger LOGGER = LoggerFactory.getLogger(VolatileVisibility.class);
public static void main(String[] args) {
// 验证可见性
visibility();
}
public static void visibility() {
Count count = new Count();
// 将num初始值设置为0
count.num = 0;
new Thread(() -> {
// 线程1休眠3秒
sleep(3000);
// 休眠3秒后, 将num + 1
count.add(1);
}, "thread 1").start();
new Thread(() -> {
// 循环
while (true) {
if (count.getNum() == 1) {
// 如果线程2获取到线程1修改完成的num, 则打印后跳出while循环
LOGGER.info("current num value: {}", count.getNum());
break;
}
}
}, "thread 2").start();
// main线程等待5秒后结束, 你也可以使用其他的方式等待上面线程执行完毕。例如CountdownLatch...之后文章都会介绍
sleep(5000);
LOGGER.info("thread main end");
}
}
a. 未加volatile关键字
上述代码中线程2先获取num=0的值,由于不满足if (count.getNum() == 1)的条件,所以无法跳出循环。三秒钟之后线程1将num的值修改为1,但是线程2无法感知将一直处于循环状态,导致程序无法停止。
b. 加了volatile关键字
上述代码中线程2先获取num=0的值,由于不满足iif (count.getNum() == 1)的条件,所以处于循环状态。三秒钟之后线程1将num的值修改为1,因为num被volatile修饰,具有可见性,num=1的修改对线程2可见,满足条件,所以线程2跳出循环,程序停止。
(2) 不保证原子性:git地址
无法保证变量同时只能被一个线程修改,也就是无法保证线程安全。(代码可以不看,就是多个线程操作同一个共享变量)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* VolatileNonAtomicity
* 不保证原子性验证
*
* @author wxy
* @date 2023-02-23
*/
public class VolatileNonAtomicity extends AbstractVolatile {
private static final Logger LOGGER = LoggerFactory.getLogger(VolatileNonAtomicity.class);
public static void main(String[] args) {
// 验证不保证原子性
nonAtomicity();
}
public static void nonAtomicity() {
Count count = new Count();
for (int num = 0; num < 10; num++) {
// 创建10个线程
new Thread(() -> {
for (int index = 0; index < 20; index++) {
// 循环十次, 每次循环+1
count.add(1);
// 休眠1毫秒(我电脑性能太好, 不休眠不好看结果)
sleep(1);
}
}).start();
}
// main线程休眠3秒等待上面执行完毕, 你也可以使用其他的方式等待上面线程执行完毕。例如CountdownLatch...之后文章都会介绍
sleep(3000);
LOGGER.info("current num value: {}", count.getNum());
}
}
循环了200次,实际结果应该是200,但是最后结果是175。
(3) 禁止指令重排序:git地址
计算机在执行程序时,为了提高性能,编译器和处理器通常都会对指令做重排序。这会产生什么样的问题呢?
多线程环境下,由于编译器优化重排,导致不同线程中使用的变量不能保证一致性,无法确定执行的结果。volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* VolatileInstructReset
* 禁止指令重排序验证
*
* @author wxy
* @date 2023-02-23
*/
public class VolatileInstructReset extends AbstractVolatile {
private static final Logger LOGGER = LoggerFactory.getLogger(VolatileInstructReset.class);
/* 未使用volatile关键字前: n次循环后a,b同时为0 */
private static int a;
private static int b;
private static int x;
private static int y;
/* 增加volatile关键字后: a,b都符合不同时为0的预期 */
/*private static volatile int a;
private static volatile int b;
private static volatile int x;
private static volatile int y;*/
public static void main(String[] args) throws InterruptedException {
// 指令重排
instructReset();
}
public static void instructReset() throws InterruptedException {
long startTime = System.currentTimeMillis();
for (; ; ) {
/* 定义四个数组, 每个里面仅有一个元素并且初始值为0 */
a = 0;
b = 0;
x = 0;
y = 0;
Thread thread1 = new Thread(() -> {
// 将x赋值为1, 执行后x=1
x = 1;
// 将a赋值为y, 如果线程1先执行那么a=0, 如果线程2先执行那么a=1
a = y;
}, "线程1");
Thread thread2 = new Thread(() -> {
// 将y赋值为1, 执行后y=1
y = 1;
// 将b赋值为x, 如果线程1先执行那么b=1, 如果线程2先执行那么b=0
b = x;
}, "线程2");
thread1.start();
thread2.start();
thread1.join();
thread2.join();
if (a == 0 && b == 0) {
// 理论上不管是先执行线程1还是先执行线程2, a[0] & b[0]不可能同时为0
// 实际上确有同时为0的可能
LOGGER.info("a: {}, b: {}", a, b);
break;
}
}
LOGGER.info("execute end, cost time: {}", System.currentTimeMillis() - startTime);
}
}
a. 未加volatile关键字
按照代码中的逻辑,a和b不可能同时为0。但是在多线程下,a和b竟然同时为0,说明在多线程环境下,指令重排使得变量执行或赋值顺序被修改。
b. 加了volatile关键字
测试了两次,每次大概四十分钟(手动停了),a和b都不同时为0,说明多线程环境下,使用volatile关键字修饰后,执行或赋值顺序没有被修改。
二、实际应用
1. 案例一
标签:count,线程,Java,num,详解,static,private,volatile From: https://blog.51cto.com/u_15898747/6142187