深入Volatile
1、变量不可见性:
1.1多线程下变量的不可见性
直接上代码
/**
 * @author yourkin666
 * @date 2024/08/12/16:12
 * @description
 */
public class h1 {
    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        myClass.start();
        while (true) {
            if (myClass.isFlag()) {
                System.out.println("circle!!!");
            }
        }
    }
}
class MyClass extends Thread{
    private boolean flag = false;
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        flag = true;
        System.out.println("flag = "+ flag);
    }
    public boolean isFlag() {
        return flag;
    }
    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}
 
子线程从主内存中读取数据放到工作内存,将flag修改为true,但是此时flag的值还没有写回主内存,所以主线程读取的flag依然是false
当子线程将flag值写回主内存后,main函数里面的while(true)调用的是系统比较底层的代码,速度较快,没时间再去主存中读取flag
所以while(true)读取到的值一直是false(当然主线程可能在一个时刻去主内存读取最新的值,我们无法控制)
1.2变量不可见性内存语义
JMM(java内存模型)是针对于Java并发编程的模型
JMM规范:
- 所有共享变量(实例变量和类变量)都存在于主内存,不包含局部变量,因为局部变量是线程私有
 - 每一个线程都还有自己的工作内存,在工作内存中,保存了一份共享变量的副本
 - 线程对变量的所有操作都在工作内存中完成
 - 不同线程的工作内存是隔离的
 
可见性问题的原因:
- 所有共享变量都存在于主内存,每个线程都有自己的工作内存,而且线程读写共享数据也是通过本地内存交换的,所以导致了可见性问题
 
1.3变量不可见行的解决方案
 第一种是加锁
 第二种是使用volatile关键字
加锁:
while (true) {
            synchronized (myClass) {
            if (myClass.isFlag()) {
                System.out.println("circle!!!");
            }
       }
}
 
线程进入synchronized代码块前后的执行过程:
- 线程获得锁
 - 清空工作内存
 - 从主内存拷贝共享变量的最新值
 - 执行代码块
 - 将修改后的副本的值刷新回主内存中
 - 线程释放锁
 
volatile关键字:
    private volatile boolean flag = false;
 
工作原理:
- 子线程从主内存读取数据放在工作内存,修改flag值后,还没有写回主内存
 - 此时主线程读取到了flag值为false
 - 当子线程将flag写回主线程后,就失效工作内存中的拷贝值
 - 再次对flag进行操作时,线程会从主内存读取最新值,放入工作内存
 
volatile关键字保证不同线程对共享变量操作的可见性,也就是一个线程修改了volatile修饰的变量,当此变量写回主内存后,其他线程可以立即看到最新值
volatile修饰的变量可以在多线程并发修改下,实现线程间变量的可见性
2、volatile其他特性:
1.volatile不保证原子性
/**
 * @author yourkin666
 * @date 2024/08/12/16:12
 * @description
 */
public class h1 {
    public static void main(String[] args) {
        Runnable myClass = new MyClass();
        for (int i = 1; i <= 100 ; i++) {
            new Thread(myClass).start();
        }
    }
}
class MyClass implements Runnable{
    private volatile int count = 0;
    @Override
    public void run() {
      for (int i = 1; i <= 10000; i++) {
          count++;
          System.out.println("count >>>>" + count);
      }
    }
}
 
count++是非原子操作,起码可以被分为三步
在多线程环境下,volatile修饰的变量也是线程不安全的
要保证数据的安全性,还得是锁,或者原子类
原子类:
 java.util.concurrent.atomic(Atomic包)
 这个包中的原子操作类提供一种用法简单、性能高效、 线程安全地更新一个变量的方式
AtomicInteger:
原子型Integer ,可以实现原子更新操作:
public AtomicInter()
初始化一个默认值为0的原子型Integet    
public AtomicInter(int initialValue)
初始化一个指定值的原子型Integet
int get()
获取值    
int getAndIncrement() 
以原子方式将当前值加1,返回自增前的值
int incrementAndget() 
以原子方式将当前值加1,返回自增后的值
int addAndGet(int data)
以原子方式将输入的值与AtomicInteger里的value相加,并返回结果
int getAndSet(int value)
以原子方式设置newValue的值,并返回旧值             
 
2.禁止指令重排序
为了提升程序的运行效率,编译器和处理器常常对指令进行重排序
使用volatile可以禁止指令重排序,从而修正重排序可能带来的并发安全问题
3、volatile内存语义:
volatile写读建立的happens - before关系:
从JDK5开始,提出happens - before概念,来阐述操作之间的内存可见性。
如果一个操作的结果需要对另一个操作可见,那么这两个操作之间必须存在happens - before关系。(这里提到的两个操作既可以是在一个线程中,也可以是不同线程之间
happens - before规则:
-  
单线程规则
同一个线程中,前面的所有写操作对后面的操作可见
 -  
锁规则(synchronized、Lock等)
如果线程1解锁了monitor A ,接着线程2锁定了A,那么,线程1解锁A之前的写操作都对线程2可见
 -  
volatile变量规则
如果线程1写入volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及之前的写操作都是对线程2可见的
 -  
传递性
A对B可见,B对C可见,那么A对C可见
 -  
start()规则
假设线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A在线程B执行前对共享变量的修改,是对线程B可见(线程B启动后的,线程A对变量的修改,线程B就未必看得到了
 -  
join()规则
线程t1写入的所有变量,在任意其他线程t2调用t1.join(),或者t1.isAlive()成功返回后,都对t2可见
 









