Java中Volatile关键字

57 篇文章 4 订阅


Java内存模型即Java Memory Model,简称JMM。JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。

CPU缓存模型

在这里插入图片描述
java线程模型根CPU模型类似,是基于CPU的缓存模型来建立的,java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
在这里插入图片描述

JMM数据原子操作

  • read(读取):从主内存读取数据
  • load(载入):将主内存读取到的数据写入工作内存
  • use(使用):从工作内存读取数据来计算
  • assign(赋值):将计算好的值重新赋值到工作内存中
  • store(存储):将工作内存数据写入主内存
  • write(写入):将store过去的变量值赋值给主内存中的变量
  • lock(锁定):将主内存变量加锁,标识为线程独占状态
  • unlocl(解锁):将主内存变量解锁,解锁后其他线程可以锁定改变量

可见性

可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。
在 Java 中 volatile、synchronized 和 final 实现可见性。

public class VolatileVisiableTest {

    private static   boolean initFlag = false;  //改了感知不到,副本本都是false
//    private static volatile  boolean initFlag = false;
    public static void main(String[] args) throws InterruptedException {
      //模拟一个线程等待
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("waitting data...");
                while (!initFlag){

                }
                System.out.println("============success");
            }
        }).start();

        Thread.sleep(2000);

        //模拟一个线程等待数据
        new Thread(new Runnable() {
            @Override
            public void run() {
            prepareData();
            }
        }).start();
    }

    public static void prepareData(){
        System.out.println("preparing data ...");
        initFlag = true;
        System.out.println("preparing data end...");
    }
}

结果
waitting data...
preparing data ...
preparing data end...

在这里插入图片描述
其中store 后已经写入在主内存还没有wite。

JMM缓存不一致性问题

总线加锁(性能太低)

在这里插入图片描述
cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其他cpu没法去读或写这个数据,知道这个cpu使用完数据释放锁之后其他cpu才能读取该数据。虽然解决了不同线程的可见性问题,并行执行变成串行执行,性能不行。

MESI缓存一致性协议

多个cpu从主内存读取同一个数据到各自的高速缓存,当其中某个cpu修改了缓存里的数据,该数据会马上同步回主内存,其他cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效。然后使用数据会重新从主内存读取。
在这里插入图片描述
volatile变量底层开启缓存一致性协议。

Volatile缓存可见性实现原理

底层实现主要通过汇编lock前缀指令,它会锁定折扣内存区域1的缓存(缓存行锁定)并回写到主内存。
IA-32架构软件开发者手册对lock指令的解释:
1)会将当前处理器缓存行的数据立即写回到系统内存。
2)这个写回内存的操作会引起其他CPU里缓存了改内存地址的数据无效(MESI协议)
volatile关键字在store 前加锁lock,写回主内存后unlock。
在这里插入图片描述

原子性(不保证)

并发编程三大特性:可见性,原子性,有序性。
Volatile保证可见性和有序性,但是不保证原子性,保证原子性需要借助synchronized这样的锁机制。其他并发写的时候


    private static final int THREADS_CONUT = 20;
    public static volatile int count = 0; //也可能<20000
//    public static AtomicInteger count = new AtomicInteger(0); //等于20000



    public static void increase() {
        count++;  //不保证原子性,某个线程count++后assign后没有拿到lock,监听总线,将工作内存count失效,丢失本次++
//        count.incrementAndGet();  //保证原子性
        System.out.println(count);
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
}

在这里插入图片描述

如何解决?1)synchronized 太重了,杀鸡用牛刀 2)juc AtomicInteger

CAS

public class AtomicIntegerTest {

    private static final int THREADS_CONUT = 200;
    public static int count = 0;   //可能<20000,线程间没有可见性,以最后写入主内存的那个线程的count值为准.
//    public static volatile int count = 0; //也可能<20000
//    public static AtomicInteger count = new AtomicInteger(0); //等于20000



    public static void increase() {
//    public synchronized static void increase() {
        count++;  //不保证原子性,某个线程count++后assign后没有拿到lock,监听总线,将工作内存count失效,丢失本次++
//        count.getAndIncrement();  //保证原子性,cas  or  count.incrementAndGet();
        System.out.println(count);
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }

    }
}

  public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }
    
  public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
  1. 假设有两个线程AB,根据上面的内存模型来看 AtomicInteger 里面的value原始值为5,即主内存中AtomicInteger的value为5,根据JMM模型,线程A和线程B各自持有一份值为5的value的副本分别到各自的工作内存。
  2. 线程A通过getIntVolatile(var1, var2)拿到value值5, 这时线程A被挂起。
  3. 线程B也通过getlntVolatile(var1, var2)方法获取到value值5, 此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为5,成功修改内存值为6,线程B打完收工,一切OK。
  4. 这时线程A恢复,执行compareAndSwapInt方法比较, 发现自己手里的值数字5和主内存的值数字6不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取新来一遍了。
  5. 线程A重新获取value值, 因为变量value被volatile修饰, 所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

cas缺点

1.获取失败一直空转(自旋),给cpu带来很大开销 2) 数量上只能保证一个变量原子性(多个加锁)3.ABA问题(AtomicStampedReference 解决ABA)

ABA

package atomic;

import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABATest {
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);

    public static void main(String[] args) {
        System.out.println("=====ABA问题产生========");
        new Thread(()->{
            atomicReference.compareAndSet(100,101);
            atomicReference.compareAndSet(101,100);
        },"t1").start();

        new Thread(()->{
            try {
                Thread.sleep(1000);

                System.out.println(Thread.currentThread().getName()+" "+atomicReference.compareAndSet(100,2019)+" "+atomicReference.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t2").start();

        try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("=====ABA问题解决========");
        new Thread(()->{

            //100->101->100
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 第一次版本号"+stamp);

            try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
            atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 第二次版本号"+atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 第三次版本号"+atomicStampedReference.getStamp());

        },"t3").start();

        new Thread(()->{
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 第一次版本号"+stamp);
            try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean result = atomicStampedReference.compareAndSet(100,2019,stamp,stamp+1);
            System.out.println(Thread.currentThread().getName()+"\t 修改成功否"+result +"\t 当前最新实际版本号:"+atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t 当前实际最新值"+atomicStampedReference.getReference());
        },"t4").start();


    }
}

有序性

有序性:在本线程内观察,操作都是有序的;如果在一个线程中观察另外一个线程,所有的操作都是无序的。
java内存模型所保证的是,同线程内,所有的操作都是由上到下的,但是多个线程并行的情况下,则不能保证其操作的有序性。
计算机在执行程序时,为了提高性能,编译器个处理器常常会对指令做重排,一般分为以下 3 种
在这里插入图片描述
单线程环境里面确保程序最终执行的结果和代码执行的结果一致
处理器在进行重排序时必须考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证用的变量能否一致性是无法确定的,结果无法预测
考试先做会做的,不会做的后做。

public void mySort(){
int x = 11; //1
int y = 12; //2
x= x+5; // 3
y = x*x;//4

可能的顺序1234 2134 1324,不可能的属性4在1 和3前,因为有数据依赖性。

在这里插入图片描述

volatile禁止指令重排。

public class ReSortSeqDemo {
    int a = 0;
    boolean flag = false;
    
    public void method01() {
        a = 1;           // flag = true;
                         // ----线程切换----
        flag = true;     // a = 1;
    }

    public void method02() {
        if (flag) {
            a = a + 3;
            System.out.println("a = " + a);
        }
    }

}

如果两个线程同时执行,method01 和 method02 如果线程 1 执行 method01 重排序了,然后切换的线程 2 执行 method02 就会出现不一样的结果。

禁止指令排序

volatile 实现禁止指令重排序的优化,从而避免了多线程环境下程序出现乱序的现象
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个 CPU 指令,他的作用有两个:

  1. 保证特定操作的执行顺序
  2. 保证某些变量的内存可见性(利用该特性实现 volatile 的内存可见性)

由于编译器个处理器都能执行指令重排序优化,如果在指令间插入一条 Memory Barrier 则会告诉编译器和 CPU,不管什么指令都不能个这条 Memory Barrier 指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后执行重排序优化。内存屏障另一个作用是强制刷出各种 CPU 缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本。
下面是保守策略下,volatile写插入内存屏障后生成的指令序列示意图:
在这里插入图片描述

下面是在保守策略下,volatile读插入内存屏障后生成的指令序列示意图:
在这里插入图片描述

volatile 性能

volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
参考:
http://blog.cuzz.site/2019/04/16/Java%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/ (volatile 是 Java 虚拟机提供的轻量级的同步机制)
https://blog.csdn.net/kingtok/article/details/105058823 (Jave 面试 CAS就这?底层与原理与自旋锁)
https://www.cnblogs.com/zhengbin/p/5654805.html(Java中Volatile关键字详解)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值