17、volatile
volatile是不错的机制,但是也不能保证原子性。
17.1. volatile 可见性
代码验证可见性
package com.interview.concurrent.volatiles;
import java.util.concurrent.TimeUnit;
/**
* @description:测试volatile的可见性
* @author DDKK.COM 弟弟快看,程序员编程资料站
* @date 2023/2/26 17:54
*/
public class VolatileVisibility {
// volatile 读取的时候去主内存中读取在最新值!
private volatile static int num = 0;
public static void main(String[] args) throws InterruptedException {
// Main线程
new Thread(()->{
/**
* @description:
* 由于num添加了volatile,所以线程每次读取都会去主内存中读取
* @author DDKK.COM 弟弟快看,程序员编程资料站
* @date 2023/2/26 17:54
*/
while (num==0){
}
}).start();
TimeUnit.SECONDS.sleep(1);
num = 1;
System.out.println(num);
}
}
17.2. volatile 不保证原子性
验证 volatile 不保证原子性
17.2.1. volatile 不保证原子性的原因分析
原子性:ACID 不可分割!完整,要么同时失败,要么同时成功!
package com.interview.concurrent.volatiles;
/**
* @author DDKK.COM 弟弟快看,程序员编程资料站
* @description 描述:验证volatile的原子性
* @date 2023/2/26 17:55
*/
public class VolatileAcid {
private volatile static int num = 0;
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
add(); // 20 * 1000 = 20000
}
},String.valueOf(i)).start();
}
// main线程等待上面执行完成,判断线程存活数 2
while (Thread.activeCount()>2){
// main gc
//线程放弃当前分得的 CPU 时间,但是不使线程阻塞
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
public static void add(){
num++;
}
}
运行效果如下:
运行效果并不是我们预期中的20000,这是什么原因呢?
因为numm++ 不是原子操作,而volatile也不保证原子性操作。
分析VolatileAcid类在堆中运行的字节码
使用命令查看类运行的字节码
javap -c xxx.class
num++被拆分成三部分,在这三者之间又会有其他进程进来读取原始值,做加1操作。怎么解决呢?看下文分解
17.2.2. volatile 不保证原子性解决方案
解决方案:
1、使用synchronized:前面已经说过;
2、使用原子性类工具java.util.concurrent.atomic
使用atomic解决原子性问题,示例代码如下:
package com.interview.concurrent.volatiles;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author DDKK.COM 弟弟快看,程序员编程资料站
* @description 描述:使用atomic解决原子性
* @date 2023/2/26 18:29
*/
public class AtomicIntegerAcid {
private volatile static AtomicInteger num = new AtomicInteger();
public static void add(){
num.getAndIncrement(); // 等价于 num++
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 1; j <= 1000; j++) {
add(); // 20 * 1000 = 20000
}
},String.valueOf(i)).start();
}
// main线程等待上面执行完成,判断线程存活数 2
while (Thread.activeCount()>2){
// main gc
//线程放弃当前分得的 CPU 时间,但是不使线程阻塞
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+num);
}
}
17.3. volatile 禁止 指令重排讲解
17.3.1. 什么是指令重排
计算机在执行程序之后,为了提高性能,编译器和处理器会进行指令重排!
处理在指令重排的时候必须要考虑数据之间的依赖性!
指令重排:程序最终执行的代码,不一定是按照你写的顺序来的!
int x = 11; // 语句1
int y = 12; // 语句2
x = y + 5; // 语句3
y = x*x ; // 语句4
//怎么执行
1234
2134
1324
// 请问语句4 能在语句3前面执行吗? 能
加深: int x,y,a,b = 0;
线程1 | 线程2 |
---|---|
x = a; | y = b; |
b = 1; | a = 2; |
x = 0, y = 0 |
假设编译器进行了指令重排,就会出现如下效果!
线程1 | 线程2 |
---|---|
b = 1; | a = 2; |
x = a; | y =b; |
x =2,y=1 |
package com.interview.concurrent.volatiles;
/**
* @author DDKK.COM 弟弟快看,程序员编程资料站
* @description 描述:指令重排
* 两个线程交替执行的!
* @date 2023/2/26 18:38
*/
public class InstructionReset {
public static void main(String[] args) {
InstructionWare instructionWare = new InstructionWare();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
instructionWare.m1();
}
},"A").start();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
instructionWare.m2();
}
},"B").start();
}
}
class InstructionWare{
int a = 0;
boolean flag = false;
public void m1(){
// A
flag = true; // 语句2
a = 1; // 语句1
}
public void m2(){
// B
if (flag){
a = a + 5; // 语句3
System.out.println("m2=>"+a);
}
}
}
由于有指令重排的问题,语句3可能在语句1先执行,这样就导致最终结果a = 5而不是6!
volatite: 能实现禁止指令重排!
17.3.2. volatile实现禁止指令重排原理:内存屏障。
volatile实现禁止指令重排原理:内存屏障。
内存屏障: 作用于CPU的指令,主要作用两个:
1、 保证特定的操作执行顺序;
2、保证某些变量的内存可见性。
禁止指令重排,能保证线程的安全性
语句1先执行,这样就导致最终结果a = 5而不是6!
volatite: 能实现禁止指令重排!