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: 能实现禁止指令重排!