1.volatile关键字

  • volatile是Java虚拟机提供的轻量级的同步机制。
    • 保证可见性
    • 不保证原子性
    • 禁止指令重排
  • volatile内存语义
    • 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
    • 当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
    • 所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取

[

](https://blog.csdn.net/TZ845195485/article/details/117601980)

1.1 可见性

1.1.1 代码证明可见性

  1. /**
  2. * 验证volatile的可见性
  3. * - 假如int number = 0; number没有用volatile修饰,没有可见性
  4. */
  5. @Slf4j(topic = "c.test1")
  6. public class Test1 {
  7. public static void main(String[] args) {
  8. Mydata mydata = new Mydata();
  9. new Thread(()->{
  10. log.debug("{} come in", Thread.currentThread().getName());
  11. try {
  12. TimeUnit.SECONDS.sleep(3);
  13. mydata.updateNumber();
  14. log.debug("{} update number value {}",Thread.currentThread().getName(), mydata.number);
  15. } catch (InterruptedException e) {
  16. e.printStackTrace();
  17. }
  18. },"t1").start();
  19. while (mydata.number == 0){
  20. // main一直等待循环,直到number不再等于0.
  21. }
  22. log.debug("{} is over,get number value {}", Thread.currentThread().getName(), mydata.number);
  23. }
  24. }
  25. class Mydata {
  26. // 不加volatile,main线程不知道这个值已经变化了.不可见,没人通知main线程
  27. // int number = 0;
  28. volatile int number = 0;
  29. public void updateNumber(){
  30. this.number = 10;
  31. }
  32. }

没有volatile关键字,3s后主线程一直在循环

  1. 21:35:58.548 [t1] DEBUG c.test1 - t1 come in
  2. 21:36:01.553 [t1] DEBUG c.test1 - t1 update number value 10

使用volatile关键字,3秒后主线程结束

  1. 21:38:57.810 [t1] DEBUG c.test1 - t1 come in
  2. 21:39:00.818 [t1] DEBUG c.test1 - t1 update number value 10
  3. 21:39:00.818 [main] DEBUG c.test1 - main is over,get number value 10

内存屏障(是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作,volatile底层的实现,写入了一条lock前缀的指令,强制写队列中的变化操作写入缓存中,根据缓存一致性协议,该变化对别的线程可见)

  • 内存屏障之前的所有写操作都要回写到主内存
  • 内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果

    1.2 原子性

    1.2.1 代码证明不保证原子性

    20个线程,每个线程执行自增方法1000次。 ```java public class Test2 {

    public static void main(String[] args) {

    1. MyData2 myData2 = new MyData2();
    2. for (int i = 0; i < 20; i++) {
    3. new Thread(()->{
    4. for (int j = 0; j < 1000; j++) {
    5. myData2.addPlusPlus();
    6. }
    7. }, String.valueOf(i)).start();
    8. }
    9. // 需要等待线程全部计算完成后再用main线程计算结果
    10. while (Thread.activeCount() > 2 ){
    11. Thread.yield();
    12. }
    13. System.out.println(Thread.currentThread().getName() + " number:" + myData2.number);

    } }

class MyData2 { volatile int number = 0;

  1. public void addPlusPlus() {
  2. number++;
  3. }

}

  1. ```java
  2. main number:18345

结果并不是20000。

  • 不保证原子性的底层解释。

volatile方式的i++,总共是四个步骤:
i++实际为load、Increment、store、Memory Barriers (内存屏障,让其他线程立即可见,且保证了顺序一致性)四个操作。在某一时刻线程1将主内存中的值i=10取出来,然后将该值放到自己的cpu高速缓存中,然后再将此值放到寄存器中,在寄存器中执行increment操作(寄存器保存的是中间值,没有直接修改i,别的线程也不会获取到这个值);此时切换到线程2对其做自增操作,读取到的依然是10,放到自己的高速缓存中,进入寄存器+1再写回到主内存。根据缓存一致性规则MESI,一旦某个线程执行了修改(Modify)操作,会立刻使其他线程的缓存失效(invaild)(其他线程并不是立即读取最新值,而volatile关键字的可见性可以保证其他线程立刻从主存中读取最新值),这样所有的线程中高速缓存获取i的值(10)失效,重新从主内存中读取11;此时切换到线程1执行,线程1此时已经完成了自增操作,将之前寄存器中的11写回到主内存中,这就造成了线程不安全(MESI只保证线程内的高速缓存数据的一致性,不保证寄存器)。

1.2.2 不保证原子性的解决方案(CAS)

  1. public class VolatileDemo {
  2. public static void main(String[] args) {
  3. MyData myData = new MyData();
  4. for (int i = 0; i < 20; i++) {
  5. new Thread(()->{
  6. for (int j = 0; j < 1000; j++) {
  7. myData.addNum();
  8. myData.addAtomicNum();
  9. }
  10. }, String.valueOf(i)).start();
  11. }
  12. while (Thread.activeCount() > 2){
  13. Thread.yield();
  14. }
  15. System.out.println(myData.num);
  16. System.out.println(myData.atomicInteger);
  17. }
  18. }
  19. class MyData{
  20. int num = 0;
  21. // 保证原子性
  22. AtomicInteger atomicInteger = new AtomicInteger();
  23. public void addNum(){
  24. this.num += 1;
  25. }
  26. public void addAtomicNum(){
  27. this.atomicInteger.getAndIncrement();
  28. }
  29. }

1.3 禁止指令重排

  • 指令重排:源代码->编译器优化重排->指令并行重排->内存系统重排->最终执行的指令
  • volatile关键字靠的是StoreStore、StoreLoad 、LoadLoad、LoadStore四条指令

image.png

  • happens-before之volatile变量规则
    • 当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前
    • 当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后
    • 当第一个操作为volatile写时,第二个操作为volatile读时,不能重排
    • 一句话:对一个volatile域的写, happens-before于任意后续对这个volatile域的读

image.png

  • 通过内存屏障实现了禁止指令重排的特性
    • volatile写
      • 在每个volatile写操作的前⾯插⼊⼀个StoreStore屏障
      • 在每个volatile写操作的后⾯插⼊⼀个StoreLoad屏障

image.png

  • 在每个volatile读操作的后⾯插⼊⼀个LoadLoad屏障
    在每个volatile读操作的后⾯插⼊⼀个LoadStore屏障

image.png

1.4 应用

1.4.1 单例模式

volatile

  1. public class SingletonDemo {
  2. private volatile static SingletonDemo instance = null;
  3. private SingletonDemo(){
  4. }
  5. public static SingletonDemo getInstance(){
  6. if (instance == null){
  7. synchronized (SingletonDemo.class){
  8. if (instance == null){
  9. instance = new SingletonDemo();
  10. }
  11. }
  12. }
  13. return instance;
  14. }
  15. }
  • 防止指令重排序,因为instance = new Singleton()不是原子操作,步骤2和步骤3不存在数据依赖关系,可能会发生重排。即3在2的前面,对象还没有初始化完成(半成品)就被赋值了,另一个线程看到后instance就不为null了,获取instance时就出问题了。
    • memory = allocate() 1.分配对象内存空间
    • instance(memory) 2.初始化对象
    • instance = memory 3.设置instance指向刚分配的内存地址
  • 保证内存可见

    使用静态内部类

  1. class SingletonDemo2 {
  2. private SingletonDemo2(){
  3. }
  4. private static class SingletonFactory{
  5. private static SingletonDemo2 singletonDemo2 = new SingletonDemo2();
  6. }
  7. public static SingletonDemo2 getInstance(){
  8. return SingletonFactory.singletonDemo2;
  9. }
  10. /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
  11. public Object readResolve() {
  12. return getInstance();
  13. }
  14. }
  • 使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。
  • 这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕, 这样我们就不用担心上面的问题。
  • 同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式。

    枚举

  1. public enum Singleton {
  2. /**
  3. * 定义一个枚举的元素,它就代表了Singleton的一个实例。
  4. */
  5. Instance;
  6. }

2.JMM(java内存模型)

2.1 可见性理解

JMM是Java内存模型,也就是Java Memory Model,简称JMM,本身是一种抽象的概念,实际上并不存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:

  • 线程解锁前,必须把共享变量的值刷新回主内存
  • 线程加锁前,必须读取主内存的最新值,到自己的工作内存
  • 加锁和解锁是同一把锁

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写会主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程:
image.png
数据传输速率:硬盘 < 内存 < < cache < CPU
上面提到了两个概念:主内存 和 工作内存

  • 主内存:就是计算机的内存,也就是经常提到的8G内存,16G内存
  • 工作内存:但我们实例化 new student,那么 age = 25 也是存储在主内存中
    • 当同时有三个线程同时访问 student中的age变量时,那么每个线程都会拷贝一份,到各自的工作内存,从而实现了变量的拷贝

image.png
即:JMM内存模型的可见性,指的是当主内存区域中的值被某个线程写入更改后,其它线程会马上知晓更改后的值,并重新得到更改后的值。